Show relay status based on stats not current connection status

This commit is contained in:
Jonathan Staab 2023-02-15 09:26:53 -06:00
parent 756c2abb30
commit 9d09eeb38c
7 changed files with 97 additions and 71 deletions

View File

@ -88,8 +88,7 @@ If you like Coracle and want to support its development, you can donate sats via
- [ ] Initial user load doesn't have any relays, cache user or wait for people db to be loaded
- [ ] Shorten height of chat headers
- [ ] Custom views should combine pubkeys, relays, and topics
- [ ] Show relay status based on stats not current connection status
- Add a dot below the relay's color code on feeds?
- [ ] Are write relays the only ones that matter? User read relays only matter for global feed, or where there's no relay hints available. But if relays are navigable, this is unnecessary.
# Changelog
@ -101,6 +100,7 @@ If you like Coracle and want to support its development, you can donate sats via
- [x] Reduce how many relays replies are published to
- [x] Re-work thread layout
- [x] Color code relays
- [x] Show relay status based on stats not current connection status
## 0.2.11

View File

@ -4,7 +4,7 @@
import {find, is, identity, nthArg, pluck} from 'ramda'
import {onMount} from "svelte"
import {createMap} from 'hurdak/lib/hurdak'
import {createMap, first} from 'hurdak/lib/hurdak'
import {writable, get} from "svelte/store"
import {fly, fade} from "svelte/transition"
import {cubicInOut} from "svelte/easing"
@ -113,12 +113,12 @@
log(
'Connection stats',
pool.getConnections()
.map(({nostr: {url}, stats: s}) => `${url} ${s.timer / s.count}`)
.map(c => `${c.nostr.url} ${c.getQuality().join(' ')}`)
)
// Alert the user to any heinously slow connections
slowConnections = pool.getConnections()
.filter(({nostr: {url}, stats: s}) => relayUrls.includes(url) && s.timer / s.count > 3000)
.filter(c => relayUrls.includes(c.nostr.url) && first(c.getQuality()) < 0.3)
}
const retrieveRelayMeta = async () => {

View File

@ -10,6 +10,14 @@ import database from 'src/agent/database'
const connections = []
const CONNECTION_STATUS = {
NEW: 'new',
ERROR: 'error',
PENDING: 'pending',
CLOSED: 'closed',
READY: 'ready',
}
class Connection {
promise: Promise<void>
nostr: Relay
@ -21,34 +29,36 @@ class Connection {
this.nostr = relayInit(url)
this.status = 'new'
this.stats = {
count: 0,
timer: 0,
timeouts: 0,
activeCount: 0,
subCount: 0,
eoseCount: 0,
eoseTimer: 0,
eventsCount: 0,
activeSubsCount: 0,
}
connections.push(this)
}
async connect() {
const shouldConnect = (
this.status === 'new'
this.status === CONNECTION_STATUS.NEW
|| (
this.status === 'error'
this.status === CONNECTION_STATUS.ERROR
&& Date.now() - this.lastConnectionAttempt > 60_000
)
)
if (shouldConnect) {
this.status = 'pending'
this.status = CONNECTION_STATUS.PENDING
this.promise = this.nostr.connect()
}
if (this.status === 'pending') {
if (this.status === CONNECTION_STATUS.PENDING) {
try {
await this.promise
this.status = 'ready'
this.status = CONNECTION_STATUS.READY
} catch (e) {
this.status = 'error'
this.status = CONNECTION_STATUS.ERROR
}
}
@ -57,7 +67,7 @@ class Connection {
return this
}
async disconnect() {
this.status = 'closed'
this.status = CONNECTION_STATUS.CLOSED
try {
await this.nostr.close()
@ -65,6 +75,39 @@ class Connection {
// For some reason bugsnag is saying this.nostr is undefined, even if we check it
}
}
getQuality() {
if (this.status === CONNECTION_STATUS.ERROR) {
return [0, "Failed to connect"]
}
const {timeouts, subCount, eoseTimer, eoseCount} = this.stats
const timeoutRate = subCount > 10 ? timeouts / subCount : null
const eoseQuality = eoseCount > 10 ? Math.max(1, 500 / (eoseTimer / eoseCount)) : null
if (timeoutRate && timeoutRate > 0.5) {
return [1 - timeoutRate, "Slow connection"]
}
if (eoseQuality && eoseQuality < 0.7) {
return [eoseQuality, "Slow connection"]
}
if (eoseQuality) {
return [eoseQuality, "Connected"]
}
if ([CONNECTION_STATUS.NEW, CONNECTION_STATUS.PENDING].includes(this.status)) {
return [0.5, "Trying to connect"]
}
if (this.status === CONNECTION_STATUS.CLOSED) {
return [0.5, "Disconnected"]
}
if (this.status === CONNECTION_STATUS.READY) {
return [1, "Connected"]
}
}
}
const getConnections = () => connections
@ -145,6 +188,7 @@ const subscribe = async (relays, filters, {onEvent, onEose}: Record<string, (e:
if (!seen.has(e.id)) {
seen.add(e.id)
conn.stats.eventsCount += 1
e.seen_on = conn.nostr.url
onEvent(e as MyEvent)
@ -160,15 +204,16 @@ const subscribe = async (relays, filters, {onEvent, onEose}: Record<string, (e:
if (!eose.has(conn.nostr.url)) {
eose.add(conn.nostr.url)
conn.stats.count += 1
conn.stats.timer += Date.now() - now
conn.stats.eoseCount += 1
conn.stats.eoseTimer += Date.now() - now
}
})
}
conn.stats.activeCount += 1
conn.stats.subsCount += 1
conn.stats.activeSubsCount += 1
if (conn.stats.activeCount > 10) {
if (conn.stats.activeSubsCount > 10) {
warn(`Relay ${conn.nostr.url} has >10 active subscriptions`)
}
@ -187,7 +232,7 @@ const subscribe = async (relays, filters, {onEvent, onEose}: Record<string, (e:
sub.unsub()
}
sub.conn.stats.activeCount -= 1
sub.conn.stats.activeSubsCount -= 1
}
})
},
@ -217,6 +262,14 @@ const subscribeUntilEose = async (
const timedOutRelays = without(Array.from(eose), relays)
log(`Timing out ${timedOutRelays.length} relays after ${timeout}ms`, timedOutRelays)
timedOutRelays.forEach(url => {
const conn = findConnection(url)
if (conn) {
conn.stats.timeouts += 1
}
})
}
if (isComplete || isTimeout) {

View File

@ -3,7 +3,7 @@
import {last, find, propEq} from 'ramda'
import {onMount} from 'svelte'
import {poll, stringToColor} from "src/util/misc"
import {switcher} from 'hurdak/lib/hurdak'
import {between} from 'hurdak/lib/hurdak'
import {fly} from 'svelte/transition'
import Toggle from "src/partials/Toggle.svelte"
import {user} from "src/agent/helpers"
@ -14,22 +14,25 @@
export let theme = 'dark'
export let showControls = false
let status = null
let quality = null
let message = null
let showStatus = false
let joined = false
$: joined = find(propEq('url', relay.url), $user?.relays || [])
onMount(() => {
return poll(300, async () => {
const conn = find(propEq('url', relay.url), pool.getConnections())
return poll(10_000, async () => {
const conn = await pool.findConnection(relay.url)
if (conn) {
const slow = conn.status === 'ready' && conn.stats.timer / conn.stats.count > 1000
// Be more strict here than with alerts
status = slow ? 'slow' : conn.status
[quality, message] = conn.getQuality()
} else {
quality = null
message = "Not connected"
}
console.log(quality, message)
})
})
</script>
@ -45,28 +48,20 @@
<div class="flex gap-2 items-center text-xl">
<i class={relay.url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
<span>{last(relay.url.split('://'))}</span>
{#if joined}
<span
on:mouseout={() => {showStatus = false}}
on:mouseover={() => {showStatus = true}}
class="w-2 h-2 rounded-full bg-medium cursor-pointer"
class:bg-danger={['pending', 'error'].includes(status)}
class:bg-warning={status === 'slow'}
class:bg-success={status === 'ready'}>
class:bg-danger={quality <= 0.3}
class:bg-warning={between(0.3, 0.7, quality)}
class:bg-success={quality > 0.7}>
</span>
<p
class="text-light text-sm transition-all hidden sm:block"
class:opacity-0={!showStatus}
class:opacity-1={showStatus}>
{switcher(status, {
error: 'Not connected',
pending: 'Trying to connect',
slow: 'Slow connection',
ready: 'Connected',
default: 'Waiting to reconnect',
})}
{message}
</p>
{/if}
</div>
{#if joined}
<button class="flex gap-3 items-center text-light" on:click={() => removeRelay(relay.url)}>

View File

@ -1,5 +1,5 @@
<script lang="ts">
import {flatten, reverse} from 'ramda'
import {flatten} from 'ramda'
import {fly} from 'svelte/transition'
import {logs} from 'src/util/logger.js'
import {formatTimestamp} from 'src/util/misc'
@ -7,7 +7,7 @@
</script>
<Content>
{#each reverse(flatten($logs)) as {created_at, message}}
{#each flatten($logs) as {created_at, message}}
<div in:fly={{y: 20}} class="text-sm flex flex-col gap-2">
<div class="text-light underline">{formatTimestamp(created_at/1000)}</div>
<pre>{message.map(m => JSON.stringify(m, null, 2)).join(' ')}</pre>

View File

@ -1,23 +1,20 @@
<script>
import {pluck, objOf, fromPairs} from 'ramda'
import {pluck, objOf} from 'ramda'
import {noop, createMap} from 'hurdak/lib/hurdak'
import {onMount} from 'svelte'
import {get} from 'svelte/store'
import {fly} from 'svelte/transition'
import {fuzzy, poll} from "src/util/misc"
import {fuzzy} from "src/util/misc"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import {user} from "src/agent/helpers"
import database from 'src/agent/database'
import pool from 'src/agent/pool'
import {modal, settings} from "src/app"
import defaults from "src/agent/defaults"
let q = ""
let search
let status = {}
let relays = []
fetch(get(settings).dufflepudUrl + '/relay')
@ -32,32 +29,13 @@
const knownRelays = database.watch('relays', relays => relays.all())
$: {
const joined = pluck('url', $user?.relays || [])
const data = ($knownRelays || []).filter(r => !joined.includes(r.url))
relays = $user?.relays || []
const joined = new Set(pluck('url', relays))
const data = ($knownRelays || []).filter(r => !joined.has(r.url))
search = fuzzy(data, {keys: ["name", "description", "url"]})
}
onMount(() => {
return poll(1000, async () => {
relays = ($user?.relays || [])
.map(relay => ({...database.relays.get(relay.url), ...relay}))
// Attempt to connect so we can show status
relays.forEach(relay => pool.connect(relay.url))
status = fromPairs(
pool.getConnections().map(({url, status, stats}) => {
// Be more strict here than with alerts
if (status === 'ready' && stats.timer / stats.count > 1000) {
status = 'slow'
}
return [url, status]
})
)
})
})
</script>
<Content>

View File

@ -3,7 +3,7 @@ import {writable} from 'svelte/store'
export const logs = writable([])
const logAndAppend = (level, ...message) => {
logs.update($logs => $logs.concat({created_at: Date.now(), message}))
logs.update($logs => $logs.concat({created_at: Date.now(), message}).slice(-100))
console[level](...message)
}