Re-work connections and relay stats

This commit is contained in:
Jon Staab
2024-11-05 12:05:04 -08:00
parent 770ce1a617
commit ecb08cace9
19 changed files with 589 additions and 508 deletions
+124 -40
View File
@@ -1,29 +1,56 @@
import {writable, derived} from 'svelte/store'
import {withGetter} from '@welshman/store'
import {ctx, groupBy, indexBy, batch, now, uniq, batcher, postJson} from '@welshman/lib'
import {ctx, groupBy, indexBy, batch, now, ago, uniq, batcher, postJson} from '@welshman/lib'
import type {RelayProfile} from "@welshman/util"
import {normalizeRelayUrl, displayRelayUrl, displayRelayProfile} from "@welshman/util"
import {asMessage, type Connection, type SocketMessage} from '@welshman/net'
import {ConnectionEvent} from '@welshman/net'
import type {Connection, Message} from '@welshman/net'
import {collection} from './collection'
export type RelayStats = {
first_seen: number
event_count: number
request_count: number
publish_count: number
connect_count: number
recent_errors: number[]
open_count: number
close_count: number
publish_count: number
request_count: number
event_count: number
last_open: number
last_close: number
last_error: number
last_publish: number
last_request: number
last_event: number
last_auth: number
publish_timer: number
publish_success_count: number
publish_failure_count: number
eose_count: number
eose_timer: number
notice_count: number
}
// Relays
export const makeRelayStats = (): RelayStats => ({
first_seen: now(),
event_count: 0,
request_count: 0,
publish_count: 0,
connect_count: 0,
recent_errors: [],
open_count: 0,
close_count: 0,
publish_count: 0,
request_count: 0,
event_count: 0,
last_open: 0,
last_close: 0,
last_error: 0,
last_publish: 0,
last_request: 0,
last_event: 0,
last_auth: 0,
publish_timer: 0,
publish_success_count: 0,
publish_failure_count: 0,
eose_count: 0,
eose_timer: 0,
notice_count: 0,
})
export type Relay = {
@@ -106,52 +133,109 @@ const updateRelayStats = batch(500, (updates: RelayStatsUpdate[]) => {
if (!$relay.stats) {
$relay.stats = makeRelayStats()
} else if ($relay.stats.notice_count === undefined) {
// Migrate from old stats
$relay.stats = {...makeRelayStats(), ...$relay.stats}
}
for (const [_, update] of items) {
update($relay.stats)
}
$relaysByUrl.set(url, $relay)
// Copy so the database gets updated, since we're mutating in updates
$relaysByUrl.set(url, {...$relay})
}
return Array.from($relaysByUrl.values())
})
})
const onConnectionSend = ({url}: Connection, socketMessage: SocketMessage) => {
const [verb] = asMessage(socketMessage)
if (verb === 'REQ') {
updateRelayStats([url, stats => ++stats.request_count])
} else if (verb === 'EVENT') {
updateRelayStats([url, stats => ++stats.publish_count])
}
}
const onConnectionReceive = ({url}: Connection, socketMessage: SocketMessage) => {
const [verb] = asMessage(socketMessage)
if (verb === 'EVENT') {
updateRelayStats([url, stats => ++stats.event_count])
}
}
const onConnectionFault = ({url}: Connection) =>
const onConnectionOpen = ({url}: Connection) =>
updateRelayStats([url, stats => {
stats.last_open = now()
stats.open_count++
}])
const onConnectionClose = ({url}: Connection) =>
updateRelayStats([url, stats => {
stats.last_close = now()
stats.close_count++
}])
const onConnectionSend = ({url}: Connection, [verb]: Message) => {
if (verb === 'REQ') {
updateRelayStats([url, stats => {
stats.request_count++
stats.last_request = now()
}])
} else if (verb === 'EVENT') {
updateRelayStats([url, stats => {
stats.publish_count++
stats.last_publish = now()
}])
}
}
const onConnectionReceive = ({url, state}: Connection, [verb, ...extra]: Message) => {
if (verb === 'OK') {
const [eventId, ok] = extra
const pub = state.pendingPublishes.get(eventId)
updateRelayStats([url, stats => {
if (pub) {
stats.publish_timer += ago(pub.sent)
}
if (ok) {
stats.publish_success_count++
} else {
stats.publish_failure_count++
}
}])
} else if (verb === 'AUTH') {
updateRelayStats([url, stats => {
stats.last_auth = now()
}])
} else if (verb === 'EVENT') {
updateRelayStats([url, stats => {
stats.event_count++
stats.last_event = now()
}])
} else if (verb === 'EOSE') {
const request = state.pendingRequests.get(extra[0])
// Only count the first eose
if (request && !request.eose) {
updateRelayStats([url, stats => {
stats.eose_count++
stats.eose_timer += now() - request.sent
}])
}
} else if (verb === 'NOTICE') {
updateRelayStats([url, stats => {
stats.notice_count++
}])
}
}
const onConnectionError = ({url}: Connection) =>
updateRelayStats([url, stats => {
stats.last_error = now()
stats.recent_errors = stats.recent_errors.concat(now()).slice(-10)
}])
export const trackRelayStats = (connection: Connection) => {
updateRelayStats([connection.url, stats => ++stats.connect_count])
connection.on('send', onConnectionSend)
connection.on('receive', onConnectionReceive)
connection.on('fault', onConnectionFault)
connection.on(ConnectionEvent.Open, onConnectionOpen)
connection.on(ConnectionEvent.Close, onConnectionClose)
connection.on(ConnectionEvent.Send, onConnectionSend)
connection.on(ConnectionEvent.Receive, onConnectionReceive)
connection.on(ConnectionEvent.Error, onConnectionError)
return () => {
connection.off('send', onConnectionSend)
connection.off('receive', onConnectionReceive)
connection.off('fault', onConnectionFault)
connection.off(ConnectionEvent.Open, onConnectionOpen)
connection.off(ConnectionEvent.Close, onConnectionClose)
connection.off(ConnectionEvent.Send, onConnectionSend)
connection.off(ConnectionEvent.Receive, onConnectionReceive)
connection.off(ConnectionEvent.Error, onConnectionError)
}
}
+19 -51
View File
@@ -129,7 +129,7 @@ export class Router {
getRelaysForUser = (mode?: RelayMode) => {
const pubkey = this.options.getUserPubkey?.()
return pubkey ? this.getRelaysForPubkey(pubkey) : []
return pubkey ? this.getRelaysForPubkey(pubkey, mode) : []
}
// Utilities for creating scenarios
@@ -274,10 +274,14 @@ export class RouterScenario {
}
const scoreRelay = (relay: string) => {
const quality = this.router.options.getRelayQuality?.(relay) || 1
const {getRelayQuality = always(1)} = this.router.options
const quality = getRelayQuality(relay)
const weight = relayWeights.get(relay)!
return -(quality * weight)
// Log the weight, since it's a straight count which ends up over-weighting hubs.
// Also add some random noise so that we'll occasionally pick lower quality/less
// popular relays.
return -(quality * inc(Math.log(weight)) * Math.random())
}
const relays = take(
@@ -306,51 +310,18 @@ export class RouterScenario {
// Default router options
export const getRelayQuality = (url: string) => {
const oneMinute = 60 * 1000
const oneHour = 60 * oneMinute
const oneDay = 24 * oneHour
const oneWeek = 7 * oneDay
const relay = relaysByUrl.get().get(url)
const connect_count = relay?.stats?.connect_count || 0
const recent_errors = relay?.stats?.recent_errors || []
const connection = ctx.net.pool.get(url, {autoConnect: false})
// If we haven't connected, consult our relay record and see if there has
// been a recent fault. If there has been, penalize the relay. If there have been several,
// don't use the relay.
if (!connection) {
const lastFault = last(recent_errors) || 0
if (!relay?.stats) return 1
if (recent_errors.filter(n => n > now() - oneHour).length > 10) {
return 0
}
const {recent_errors} = relay.stats
const last_error = last(recent_errors) || 0
if (recent_errors.filter(n => n > now() - oneDay).length > 50) {
return 0
}
if (recent_errors.filter(n => n > ago(HOUR)).length > 5) return 0
if (recent_errors.filter(n => n > ago(DAY)).length > 20) return 0
if (recent_errors.filter(n => n > ago(WEEK)).length > 100) return 0
if (recent_errors.filter(n => n > now() - oneWeek).length > 100) {
return 0
}
return Math.max(0, Math.min(0.5, (now() - oneMinute - lastFault) / oneHour))
}
const authScore = switcher(connection.auth.status, {
[AuthStatus.Forbidden]: 0,
[AuthStatus.Ok]: 1,
default: 0.5,
})
const connectionScore = switcher(connection.meta.getStatus(), {
[ConnectionStatus.Error]: 0,
[ConnectionStatus.Closed]: 0.6,
[ConnectionStatus.Slow]: 0.5,
[ConnectionStatus.Ok]: 1,
default: clamp([0.5, 1], connect_count / 1000),
})
return authScore * connectionScore
return Math.max(0, Math.min(0.5, (ago(MINUTE) - last_error) / HOUR))
}
export const getPubkeyRelays = (pubkey: string, mode?: string) => {
@@ -402,9 +373,6 @@ type FilterScenario = {filter: Filter, scenario: RouterScenario}
type FilterSelectionRule = (filter: Filter) => FilterScenario[]
export const getFilterSelectionsForLocalRelay = (filter: Filter) =>
[{filter, scenario: ctx.app.router.FromRelays([LOCAL_RELAY_URL])}]
export const getFilterSelectionsForSearch = (filter: Filter) => {
if (!filter.search) return []
@@ -438,7 +406,7 @@ export const getFilterSelectionsForIndexedKinds = (filter: Filter) => {
export const getFilterSelectionsForAuthors = (filter: Filter) => {
if (!filter.authors) return []
const chunkCount = clamp([1, 4], Math.round(filter.authors.length / 50))
const chunkCount = clamp([1, 30], Math.round(filter.authors.length / 30))
return chunks(chunkCount, filter.authors)
.map(authors => ({
@@ -448,10 +416,9 @@ export const getFilterSelectionsForAuthors = (filter: Filter) => {
}
export const getFilterSelectionsForUser = (filter: Filter) =>
[{filter, scenario: ctx.app.router.ForUser().weight(0.5)}]
[{filter, scenario: ctx.app.router.ForUser().weight(0.2)}]
export const defaultFilterSelectionRules = [
getFilterSelectionsForLocalRelay,
getFilterSelectionsForSearch,
getFilterSelectionsForWraps,
getFilterSelectionsForIndexedKinds,
@@ -461,7 +428,7 @@ export const defaultFilterSelectionRules = [
export const getFilterSelections = (
filters: Filter[],
rules: FilterSelectionRule[] = defaultFilterSelectionRules
rules: FilterSelectionRule[] = defaultFilterSelectionRules
): RelaysAndFilters[] => {
const filtersById = new Map<string, Filter>()
const scenariosById = new Map<string, RouterScenario[]>()
@@ -479,8 +446,9 @@ export const getFilterSelections = (
for (const [id, filter] of filtersById.entries()) {
const scenario = ctx.app.router.merge(scenariosById.get(id) || [])
const relays = scenario.getUrls().concat(LOCAL_RELAY_URL)
result.push({filters: [filter], relays: scenario.getUrls()})
result.push({filters: [filter], relays})
}
return result
+2 -2
View File
@@ -3,7 +3,7 @@ import type {IDBPDatabase} from "idb"
import {throttle} from "throttle-debounce"
import {writable} from "svelte/store"
import type {Unsubscriber, Writable} from "svelte/store"
import {indexBy, equals, fromPairs} from "@welshman/lib"
import {indexBy, fromPairs} from "@welshman/lib"
import type {TrustedEvent, Repository} from "@welshman/util"
import type {Tracker} from "@welshman/net"
import {withGetter, adapter, throttled, custom} from "@welshman/store"
@@ -60,7 +60,7 @@ export const initIndexedDbAdapter = async (name: string, adapter: IndexedDbAdapt
const removedRecords = prevRecords.filter(r => !currentIds.has(r[adapter.keyPath]))
const prevRecordsById = indexBy(item => item[adapter.keyPath], prevRecords)
const updatedRecords = currentRecords.filter(r => !equals(r, prevRecordsById.get(r[adapter.keyPath])))
const updatedRecords = currentRecords.filter(r => r !== prevRecordsById.get(r[adapter.keyPath]))
prevRecords = currentRecords