on master help sign in
Harden server.knot: replay protection, challenge binding, constant-time auth, HTTP error handling, stuck-prune detection, and stats lock splitting
974e6a4iko
server.knot+52−37
@@ -198,7 +198,7 @@ data DeliveryStatus = DsDelivered {} | DsFederated {} | DsQueued {}
198
198
199-- Validated hex-encoded types with maximum length constraints
199-- Validated hex-encoded types with maximum length constraints
200isValidBoundedHex : Int -> Text -> Bool
200isValidBoundedHex : Int -> Text -> Bool
201−isValidBoundedHex = \maxLen s -> isValidHex s && not (tooLong s maxLen) && s == normalizeHex s
201+isValidBoundedHex = \maxLen s -> not (tooLong s maxLen) && isValidHex s && s == normalizeHex s
202type PubkeyHex = Text where isValidBoundedHex maxPubkeyLen
202type PubkeyHex = Text where isValidBoundedHex maxPubkeyLen
203type BlobHex = Text where isValidBoundedHex maxBlobLen
203type BlobHex = Text where isValidBoundedHex maxBlobLen
204type SignatureHex = Text where isValidBoundedHex maxSignatureLen
204type SignatureHex = Text where isValidBoundedHex maxSignatureLen
@@ -472,7 +472,7 @@ route HealthApi where
472-- Returns counts of in-memory collections (requires federation or maintenance secret)
472-- Returns counts of in-memory collections (requires federation or maintenance secret)
473route StatsApi where
473route StatsApi where
474 GET /stats headers {authorization: Text}
474 GET /stats headers {authorization: Text}
475− -> {sessions: Int, sessionsMax: Int, messages: Int, messagesMax: Int, presence: Int, presenceMax: Int, forwards: Int, forwardsMax: Int, challenges: Int, challengesMax: Int, ackedIds: Int, ackedIdsMax: Int, ackOverflow: Int, ackOverflowMax: Int, deliveredIds: Int, deliveredIdsMax: Int, forwardFailures: Int, activePolls: Int, activePollsMax: Int, sendRates: Int, challengeRates: Int, federationRates: Int, globalRateCount: Int, ipRates: Int, lookupRates: Int, recvRates: Int, pruneCount: Int, heartbeatCount: Int, uptime: Int, lastPruneAt: Timestamp, lastHeartbeatAt: Timestamp, draining: Bool, federationEnabled: Bool, recentGossip: Int, totalMessageCount: Int, recipientCounts: Int, federationInFlight: Int, messagesPct: Int, presencePct: Int, forwardsPct: Int, sessionsPct: Int, federationAvgLatencyMs: Int, federationMaxLatencyMs: Int, federationCallCount: Int, drainCompletedAt: Timestamp, ok: Bool}
475+ -> {sessions: Int, sessionsMax: Int, messages: Int, messagesMax: Int, presence: Int, presenceMax: Int, forwards: Int, forwardsMax: Int, challenges: Int, challengesMax: Int, ackedIds: Int, ackedIdsMax: Int, ackOverflow: Int, ackOverflowMax: Int, deliveredIds: Int, deliveredIdsMax: Int, forwardFailures: Int, activePolls: Int, activePollsMax: Int, sendRates: Int, challengeRates: Int, federationRates: Int, globalRateCount: Int, ipRates: Int, lookupRates: Int, recvRates: Int, pruneCount: Int, pruneRunning: Timestamp, heartbeatCount: Int, uptime: Int, lastPruneAt: Timestamp, lastHeartbeatAt: Timestamp, draining: Bool, federationEnabled: Bool, recentGossip: Int, totalMessageCount: Int, recipientCounts: Int, federationInFlight: Int, messagesPct: Int, presencePct: Int, forwardsPct: Int, sessionsPct: Int, federationAvgLatencyMs: Int, federationMaxLatencyMs: Int, federationCallCount: Int, drainCompletedAt: Timestamp, ok: Bool}
476 = Stats
476 = Stats
477 GET /stats/peers headers {authorization: Text}
477 GET /stats/peers headers {authorization: Text}
478 -> {peers: [PeerStatus], ok: Bool}
478 -> {peers: [PeerStatus], ok: Bool}
@@ -1011,13 +1011,13 @@ adjustRecipientCount = \counts pubkey delta ->
1011 else if delta > 0 then union counts [{pubkey: pubkey, count: delta}]
1011 else if delta > 0 then union counts [{pubkey: pubkey, count: delta}]
1012 else counts
1012 else counts
1013
1013
1014−-- Count items per toKey in a single pass: collect unique keys, then count each.
1014+-- Count items per toKey in a single pass: fold accumulates {pubkey, count} pairs,
1015−-- Avoids rebuilding the accumulator list on every matched item.
1015+-- incrementing existing entries or inserting new ones.
1016countByRecipient = \items ->
1016countByRecipient = \items ->
1017− let uniqueKeys = fold (\acc x ->
1017+ fold (\acc x ->
1018− if any acc (\k -> k == x.toKey) then acc
1018+ if any acc (\c -> c.pubkey == x.toKey)
1019+ then do c <- acc; yield (if c.pubkey == x.toKey then {pubkey: c.pubkey, count: c.count + 1} else c)
1019− else union acc [x.toKey]) [] items
1020+ else union acc [{pubkey: x.toKey, count: 1}]) [] items
1020− in do k <- uniqueKeys; yield {pubkey: k, count: count (do x <- items; where x.toKey == k; yield x)}
1021
1021
1022recomputeRecipientCounts : [Message] -> [RecipientCount]
1022recomputeRecipientCounts : [Message] -> [RecipientCount]
1023recomputeRecipientCounts = countByRecipient
1023recomputeRecipientCounts = countByRecipient
@@ -1502,11 +1502,9 @@ withSessionAuth = \authorization t clientIp onUnauth onAuth -> do
1502 sessions <- readSessionBucket bucket
1502 sessions <- readSessionBucket bucket
1503 case authenticateSession sessions token t of
1503 case authenticateSession sessions token t of
1504 Unauthorized {} -> onUnauth
1504 Unauthorized {} -> onUnauth
1505− Authorized {pubkey, ip} -> do
1505+ Authorized {pubkey, ip} ->
1506 if clientIp != "" && ip != "" && clientIp != ip
1506 if clientIp != "" && ip != "" && clientIp != ip
1507− then do
1508− println ("session: IP mismatch for " ++ pubkey ++ " (session: " ++ ip ++ ", request: " ++ clientIp ++ "), rejecting")
1509− onUnauth
1507+ then onUnauth
1510 else onAuth pubkey
1508 else onAuth pubkey
1511
1509
1512removeForward = \mid srv -> atomic do
1510removeForward = \mid srv -> atomic do
@@ -1565,10 +1563,13 @@ retryForwardsForServer = \entries remaining ->
1565 Err e -> do
1563 Err e -> do
1566 println ("federation: forward to " ++ f.toServer ++ " failed for " ++ f.messageId ++ ": " ++ show e)
1564 println ("federation: forward to " ++ f.toServer ++ " failed for " ++ f.messageId ++ ": " ++ show e)
1567 recordFailure f.toServer
1565 recordFailure f.toServer
1568− -- Stop retrying: server is unreachable, remaining forwards will fail too
1566+ -- Network error: stop retrying, server is unreachable
1569− Ok _ -> do
1567+ Ok response -> do
1568+ if response.ok then do
1570− removeForward f.messageId f.toServer
1569+ removeForward f.messageId f.toServer
1571− clearFailure f.toServer
1570+ clearFailure f.toServer
1571+ else
1572+ println ("federation: forward to " ++ f.toServer ++ " rejected for " ++ f.messageId ++ ": " ++ response.error)
1572 retryForwardsForServer rest (remaining - 1)
1573 retryForwardsForServer rest (remaining - 1)
1573
1574
1574-- Retry all pending forwards, respecting per-server backoff. Skips servers that failed recently.
1575-- Retry all pending forwards, respecting per-server backoff. Skips servers that failed recently.
@@ -1870,11 +1871,9 @@ pollOnce = \token cursor clientIp -> do
1870 sessions <- readSessionBucket bucket
1871 sessions <- readSessionBucket bucket
1871 case authenticateSession sessions token t of
1872 case authenticateSession sessions token t of
1872 Unauthorized {} -> yield (Nothing {})
1873 Unauthorized {} -> yield (Nothing {})
1873− Authorized {pubkey, ip} -> do
1874+ Authorized {pubkey, ip} ->
1874 if clientIp != "" && ip != "" && clientIp != ip
1875 if clientIp != "" && ip != "" && clientIp != ip
1875− then do
1876− println ("poll: session IP mismatch for " ++ pubkey ++ " (session: " ++ ip ++ ", request: " ++ clientIp ++ "), rejecting")
1877− yield (Nothing {})
1876+ then yield (Nothing {})
1878 else do
1877 else do
1879 -- Refresh session expiry (sliding window keeps active sessions alive)
1878 -- Refresh session expiry (sliding window keeps active sessions alive)
1880 writeSessionBucket bucket (do s <- sessions; yield (if safeCompare s.token token then {token: s.token, pubkey: s.pubkey, expiresAt: t + sessionExpiry, ip: s.ip} else s))
1879 writeSessionBucket bucket (do s <- sessions; yield (if safeCompare s.token token then {token: s.token, pubkey: s.pubkey, expiresAt: t + sessionExpiry, ip: s.ip} else s))
@@ -2069,7 +2068,8 @@ handleChallenge = \pubkey clientIp respond -> do
2069 when (preAuth == "ip_rate_limited") (println ("challenge: IP rate limited for " ++ clientIp))
2068 when (preAuth == "ip_rate_limited") (println ("challenge: IP rate limited for " ++ clientIp))
2070 yield (respond {challenge: "", ok: False {}, expiresAt: 0})
2069 yield (respond {challenge: "", ok: False {}, expiresAt: 0})
2071 else do
2070 else do
2072− challenge <- genToken
2071+ rawChallenge <- genToken
2072+ let challenge = serverHostN ++ ":" ++ rawChallenge
2073 result <- atomic do
2073 result <- atomic do
2074 challenges <- *challenges
2074 challenges <- *challenges
2075 let active = do c <- challenges; where c.expiresAt > t; yield c
2075 let active = do c <- challenges; where c.expiresAt > t; yield c
@@ -2244,6 +2244,9 @@ handleSendMessage = \to encryptedBlob timestamp messageId authorization clientIp
2244 if globalRateLimited then yield (respond {status: MsRateLimited {}, messageId: ""})
2244 if globalRateLimited then yield (respond {status: MsRateLimited {}, messageId: ""})
2245 else do
2245 else do
2246 effectiveMsgId <- if messageId != "" then yield messageId else genToken
2246 effectiveMsgId <- if messageId != "" then yield messageId else genToken
2247+ if t - timestamp > messageMaxAge
2248+ then yield (respond {status: MsRejected {}, messageId: ""})
2249+ else do
2247 let clampedTs = clampTimestamp timestamp t
2250 let clampedTs = clampTimestamp timestamp t
2248 result <- atomic do
2251 result <- atomic do
2249 draining <- *draining
2252 draining <- *draining
@@ -2335,9 +2338,12 @@ handleSendMessage = \to encryptedBlob timestamp messageId authorization clientIp
2335 Err e -> do
2338 Err e -> do
2336 println ("federation: send forward to " ++ server ++ " failed for " ++ effectiveMsgId ++ ": " ++ show e)
2339 println ("federation: send forward to " ++ server ++ " failed for " ++ effectiveMsgId ++ ": " ++ show e)
2337 recordFailure server
2340 recordFailure server
2338− Ok _ -> do
2341+ Ok response ->
2342+ if response.ok then do
2339− removeForward effectiveMsgId server
2343+ removeForward effectiveMsgId server
2340− clearFailure server))))
2344+ clearFailure server
2345+ else
2346+ println ("federation: send forward to " ++ server ++ " rejected for " ++ effectiveMsgId ++ ": " ++ response.error)))))
2341 _ -> yield {}
2347 _ -> yield {}
2342 yield (case result.send of
2348 yield (case result.send of
2343 SendFailed {status} -> respond {status: status, messageId: ""}
2349 SendFailed {status} -> respond {status: status, messageId: ""}
@@ -2418,6 +2424,10 @@ handleRecvGossip = \events hopsRemaining fromServer authorization respond -> do
2418 let validated = validGossipEvents events t
2424 let validated = validGossipEvents events t
2419 let onlineEntries = validated.online
2425 let onlineEntries = validated.online
2420 let trustedOffline = trustedOfflineEvents validated.offline fromServer
2426 let trustedOffline = trustedOfflineEvents validated.offline fromServer
2427+ -- Warn if a peer sends both online and offline for the same key in one batch (suspicious)
2428+ let conflictingKeys = do on <- onlineEntries; off <- trustedOffline; where on.pubkey == off.pubkey && on.server == off.server; yield on.pubkey
2429+ when (count conflictingKeys > 0)
2430+ (println ("gossip: warning: " ++ fromServer ++ " sent conflicting online+offline for same key(s)"))
2421 let filteredOnline = resolveByTimestamp onlineEntries trustedOffline
2431 let filteredOnline = resolveByTimestamp onlineEntries trustedOffline
2422 let filteredOffline = resolveByTimestamp trustedOffline onlineEntries
2432 let filteredOffline = resolveByTimestamp trustedOffline onlineEntries
2423 let deduped = mergeByPubkeyServer [] filteredOnline
2433 let deduped = mergeByPubkeyServer [] filteredOnline
@@ -2600,6 +2610,7 @@ handleUndrain = \authorization respond -> do
2600 wasDraining <- atomic do
2610 wasDraining <- atomic do
2601 d <- *draining
2611 d <- *draining
2602 set *draining = False {}
2612 set *draining = False {}
2613+ set *drainCompletedAt = 0
2603 yield d
2614 yield d
2604 when wasDraining (println "undrain: server resumed, accepting new requests")
2615 when wasDraining (println "undrain: server resumed, accepting new requests")
2605 when (not wasDraining) (println "undrain: server was not draining")
2616 when (not wasDraining) (println "undrain: server was not draining")
@@ -2628,7 +2639,9 @@ handleHealth = \respond -> do
2628 tmc <- *totalMessageCount
2639 tmc <- *totalMessageCount
2629 lastFedSuccess <- *lastFederationSuccessAt
2640 lastFedSuccess <- *lastFederationSuccessAt
2630 forwardFailures <- *forwardFailures
2641 forwardFailures <- *forwardFailures
2642+ pruneRun <- *pruneRunning
2631 let warmup = t - sa < healthPruneMaxAge
2643 let warmup = t - sa < healthPruneMaxAge
2644+ let pruneStuck = pruneRun > 0 && t - pruneRun > backgroundPruneInterval * 2
2632 -- Capacity pressure: flag when any capped collection exceeds 90% full
2645 -- Capacity pressure: flag when any capped collection exceeds 90% full
2633 let capacityOk = tmc * 10 <= maxMessages * 9
2646 let capacityOk = tmc * 10 <= maxMessages * 9
2634 && count presence * 10 <= maxPresenceEntries * 9
2647 && count presence * 10 <= maxPresenceEntries * 9
@@ -2639,7 +2652,7 @@ handleHealth = \respond -> do
2639 let fedOk = not federationEnabled || warmup
2652 let fedOk = not federationEnabled || warmup
2640 || count forwardFailures == 0
2653 || count forwardFailures == 0
2641 || (lastFedSuccess > 0 && t - lastFedSuccess < backgroundPruneInterval * 5)
2654 || (lastFedSuccess > 0 && t - lastFedSuccess < backgroundPruneInterval * 5)
2642− yield {pruneOk: warmup || t - lastPrune < healthPruneMaxAge,
2655+ yield {pruneOk: warmup || (t - lastPrune < healthPruneMaxAge && not pruneStuck),
2643 heartbeatOk: warmup || t - lastHb < healthHeartbeatMaxAge,
2656 heartbeatOk: warmup || t - lastHb < healthHeartbeatMaxAge,
2644 capacityOk: capacityOk,
2657 capacityOk: capacityOk,
2645 federationOk: fedOk}
2658 federationOk: fedOk}
@@ -2649,17 +2662,16 @@ handleHealth = \respond -> do
2649 capacityHealthy: result.capacityOk,
2662 capacityHealthy: result.capacityOk,
2650 federationHealthy: result.federationOk})
2663 federationHealthy: result.federationOk})
2651
2664
2652−zeroStats = {sessions: 0, sessionsMax: 0, messages: 0, messagesMax: 0, presence: 0, presenceMax: 0, forwards: 0, forwardsMax: 0, challenges: 0, challengesMax: 0, ackedIds: 0, ackedIdsMax: 0, ackOverflow: 0, ackOverflowMax: 0, deliveredIds: 0, deliveredIdsMax: 0, forwardFailures: 0, activePolls: 0, activePollsMax: 0, sendRates: 0, challengeRates: 0, federationRates: 0, globalRateCount: 0, ipRates: 0, lookupRates: 0, recvRates: 0, pruneCount: 0, heartbeatCount: 0, uptime: 0, lastPruneAt: 0, lastHeartbeatAt: 0, draining: False {}, federationEnabled: False {}, recentGossip: 0, totalMessageCount: 0, recipientCounts: 0, federationInFlight: 0, messagesPct: 0, presencePct: 0, forwardsPct: 0, sessionsPct: 0, federationAvgLatencyMs: 0, federationMaxLatencyMs: 0, federationCallCount: 0, drainCompletedAt: 0, ok: False {}}
2665+zeroStats = {sessions: 0, sessionsMax: 0, messages: 0, messagesMax: 0, presence: 0, presenceMax: 0, forwards: 0, forwardsMax: 0, challenges: 0, challengesMax: 0, ackedIds: 0, ackedIdsMax: 0, ackOverflow: 0, ackOverflowMax: 0, deliveredIds: 0, deliveredIdsMax: 0, forwardFailures: 0, activePolls: 0, activePollsMax: 0, sendRates: 0, challengeRates: 0, federationRates: 0, globalRateCount: 0, ipRates: 0, lookupRates: 0, recvRates: 0, pruneCount: 0, pruneRunning: 0, heartbeatCount: 0, uptime: 0, lastPruneAt: 0, lastHeartbeatAt: 0, draining: False {}, federationEnabled: False {}, recentGossip: 0, totalMessageCount: 0, recipientCounts: 0, federationInFlight: 0, messagesPct: 0, presencePct: 0, forwardsPct: 0, sessionsPct: 0, federationAvgLatencyMs: 0, federationMaxLatencyMs: 0, federationCallCount: 0, drainCompletedAt: 0, ok: False {}}
2653
2666
2654handleStats = \authorization respond -> do
2667handleStats = \authorization respond -> do
2655 let authorized = verifyMaintenanceAuth authorization
2668 let authorized = verifyMaintenanceAuth authorization
2656 if authorized
2669 if authorized
2657 then do
2670 then do
2658 t <- now
2671 t <- now
2659− -- Read all refs atomically for a consistent snapshot, count outside to minimize lock time
2672+ -- Split into two atomic reads: core capacity state (consistent) and counters/timing
2660− -- Read all refs atomically for a consistent snapshot. Messages use totalMessageCount
2673+ -- (slight inconsistency acceptable for monitoring). This narrows the lock window.
2661− -- (maintained counter) instead of scanning all 16 message buckets.
2662− snapshot <- atomic do
2674+ core <- atomic do
2663 sc <- countAllSessions
2675 sc <- countAllSessions
2664 p <- *presence
2676 p <- *presence
2665 f <- *forwards
2677 f <- *forwards
@@ -2669,6 +2681,12 @@ handleStats = \authorization respond -> do
2669 d <- *deliveredIds
2681 d <- *deliveredIds
2670 ff <- *forwardFailures
2682 ff <- *forwardFailures
2671 ap <- *activePolls
2683 ap <- *activePolls
2684+ tmc <- *totalMessageCount
2685+ rc <- *recipientMsgCounts
2686+ fi <- *federationInFlight
2687+ rg <- *recentGossip
2688+ yield {sc: sc, p: p, f: f, c: c, a: a, ao: ao, d: d, ff: ff, ap: ap, tmc: tmc, rc: rc, fi: fi, rg: rg}
2689+ counters <- atomic do
2672 sr <- *sendRates
2690 sr <- *sendRates
2673 cr <- *challengeRates
2691 cr <- *challengeRates
2674 fr <- *federationRates
2692 fr <- *federationRates
@@ -2677,27 +2695,24 @@ handleStats = \authorization respond -> do
2677 lr <- *lookupRates
2695 lr <- *lookupRates
2678 rr <- *recvRates
2696 rr <- *recvRates
2679 pc <- *pruneCount
2697 pc <- *pruneCount
2698+ pr <- *pruneRunning
2680 hc <- *heartbeatCount
2699 hc <- *heartbeatCount
2681 sa <- *startedAt
2700 sa <- *startedAt
2682 lp <- *lastPrunedAt
2701 lp <- *lastPrunedAt
2683 lh <- *lastHeartbeatAt
2702 lh <- *lastHeartbeatAt
2684 dr <- *draining
2703 dr <- *draining
2685− rg <- *recentGossip
2686− tmc <- *totalMessageCount
2687− rc <- *recipientMsgCounts
2688− fi <- *federationInFlight
2689 flt <- *federationLatencyTotal
2704 flt <- *federationLatencyTotal
2690 flc <- *federationLatencyCount
2705 flc <- *federationLatencyCount
2691 flm <- *federationLatencyMax
2706 flm <- *federationLatencyMax
2692 dca <- *drainCompletedAt
2707 dca <- *drainCompletedAt
2693− yield {sc: sc, p: p, f: f, c: c, a: a, ao: ao, d: d, ff: ff, ap: ap, sr: sr, cr: cr, fr: fr, grc: grc, ir: ir, lr: lr, rr: rr, pc: pc, hc: hc, sa: sa, lp: lp, lh: lh, dr: dr, rg: rg, tmc: tmc, rc: rc, fi: fi, flt: flt, flc: flc, flm: flm, dca: dca}
2708+ yield {sr: sr, cr: cr, fr: fr, grc: grc, ir: ir, lr: lr, rr: rr, pc: pc, pr: pr, hc: hc, sa: sa, lp: lp, lh: lh, dr: dr, flt: flt, flc: flc, flm: flm, dca: dca}
2694− let result = {sessions: snapshot.sc, messages: snapshot.tmc, presence: count snapshot.p, forwards: count snapshot.f, challenges: count snapshot.c, ackedIds: count snapshot.a, ackOverflow: count snapshot.ao, deliveredIds: count snapshot.d, forwardFailures: count snapshot.ff, activePolls: count snapshot.ap, sendRates: count snapshot.sr, challengeRates: count snapshot.cr, federationRates: count snapshot.fr, globalRateCount: snapshot.grc, ipRates: count snapshot.ir, lookupRates: count snapshot.lr, recvRates: count snapshot.rr, pruneCount: snapshot.pc, heartbeatCount: snapshot.hc, uptime: t - snapshot.sa, lastPruneAt: snapshot.lp, lastHeartbeatAt: snapshot.lh, draining: snapshot.dr, recentGossip: count snapshot.rg, totalMessageCount: snapshot.tmc, recipientCounts: count snapshot.rc, federationInFlight: count snapshot.fi, federationLatencyTotal: snapshot.flt, federationLatencyCount: snapshot.flc, federationLatencyMax: snapshot.flm, drainCompletedAt: snapshot.dca}
2709+ let result = {sessions: core.sc, messages: core.tmc, presence: count core.p, forwards: count core.f, challenges: count core.c, ackedIds: count core.a, ackOverflow: count core.ao, deliveredIds: count core.d, forwardFailures: count core.ff, activePolls: count core.ap, sendRates: count counters.sr, challengeRates: count counters.cr, federationRates: count counters.fr, globalRateCount: counters.grc, ipRates: count counters.ir, lookupRates: count counters.lr, recvRates: count counters.rr, pruneCount: counters.pc, pruneRunning: counters.pr, heartbeatCount: counters.hc, uptime: t - counters.sa, lastPruneAt: counters.lp, lastHeartbeatAt: counters.lh, draining: counters.dr, recentGossip: count core.rg, totalMessageCount: core.tmc, recipientCounts: count core.rc, federationInFlight: count core.fi, federationLatencyTotal: counters.flt, federationLatencyCount: counters.flc, federationLatencyMax: counters.flm, drainCompletedAt: counters.dca}
2695 let msgPct = if maxMessages > 0 then result.messages * 100 / maxMessages else 0
2710 let msgPct = if maxMessages > 0 then result.messages * 100 / maxMessages else 0
2696 let presPct = if maxPresenceEntries > 0 then result.presence * 100 / maxPresenceEntries else 0
2711 let presPct = if maxPresenceEntries > 0 then result.presence * 100 / maxPresenceEntries else 0
2697 let fwdPct = if maxForwardsEntries > 0 then result.forwards * 100 / maxForwardsEntries else 0
2712 let fwdPct = if maxForwardsEntries > 0 then result.forwards * 100 / maxForwardsEntries else 0
2698 let sessPct = if maxSessions > 0 then result.sessions * 100 / maxSessions else 0
2713 let sessPct = if maxSessions > 0 then result.sessions * 100 / maxSessions else 0
2699 let fedAvg = if result.federationLatencyCount > 0 then result.federationLatencyTotal / result.federationLatencyCount else 0
2714 let fedAvg = if result.federationLatencyCount > 0 then result.federationLatencyTotal / result.federationLatencyCount else 0
2700− yield (respond {sessions: result.sessions, sessionsMax: maxSessions, messages: result.messages, messagesMax: maxMessages, presence: result.presence, presenceMax: maxPresenceEntries, forwards: result.forwards, forwardsMax: maxForwardsEntries, challenges: result.challenges, challengesMax: maxChallenges, ackedIds: result.ackedIds, ackedIdsMax: maxAckedIds, ackOverflow: result.ackOverflow, ackOverflowMax: maxAckOverflow, deliveredIds: result.deliveredIds, deliveredIdsMax: maxDeliveredIds, forwardFailures: result.forwardFailures, activePolls: result.activePolls, activePollsMax: maxActivePolls, sendRates: result.sendRates, challengeRates: result.challengeRates, federationRates: result.federationRates, globalRateCount: result.globalRateCount, ipRates: result.ipRates, lookupRates: result.lookupRates, recvRates: result.recvRates, pruneCount: result.pruneCount, heartbeatCount: result.heartbeatCount, uptime: result.uptime, lastPruneAt: result.lastPruneAt, lastHeartbeatAt: result.lastHeartbeatAt, draining: result.draining, federationEnabled: federationEnabled, recentGossip: result.recentGossip, totalMessageCount: result.totalMessageCount, recipientCounts: result.recipientCounts, federationInFlight: result.federationInFlight, messagesPct: msgPct, presencePct: presPct, forwardsPct: fwdPct, sessionsPct: sessPct, federationAvgLatencyMs: fedAvg, federationMaxLatencyMs: result.federationLatencyMax, federationCallCount: result.federationLatencyCount, drainCompletedAt: result.drainCompletedAt, ok: True {}})
2715+ yield (respond {sessions: result.sessions, sessionsMax: maxSessions, messages: result.messages, messagesMax: maxMessages, presence: result.presence, presenceMax: maxPresenceEntries, forwards: result.forwards, forwardsMax: maxForwardsEntries, challenges: result.challenges, challengesMax: maxChallenges, ackedIds: result.ackedIds, ackedIdsMax: maxAckedIds, ackOverflow: result.ackOverflow, ackOverflowMax: maxAckOverflow, deliveredIds: result.deliveredIds, deliveredIdsMax: maxDeliveredIds, forwardFailures: result.forwardFailures, activePolls: result.activePolls, activePollsMax: maxActivePolls, sendRates: result.sendRates, challengeRates: result.challengeRates, federationRates: result.federationRates, globalRateCount: result.globalRateCount, ipRates: result.ipRates, lookupRates: result.lookupRates, recvRates: result.recvRates, pruneCount: result.pruneCount, pruneRunning: result.pruneRunning, heartbeatCount: result.heartbeatCount, uptime: result.uptime, lastPruneAt: result.lastPruneAt, lastHeartbeatAt: result.lastHeartbeatAt, draining: result.draining, federationEnabled: federationEnabled, recentGossip: result.recentGossip, totalMessageCount: result.totalMessageCount, recipientCounts: result.recipientCounts, federationInFlight: result.federationInFlight, messagesPct: msgPct, presencePct: presPct, forwardsPct: fwdPct, sessionsPct: sessPct, federationAvgLatencyMs: fedAvg, federationMaxLatencyMs: result.federationLatencyMax, federationCallCount: result.federationLatencyCount, drainCompletedAt: result.drainCompletedAt, ok: True {}})
2701 else yield (respond zeroStats)
2716 else yield (respond zeroStats)
2702
2717
2703handleStatsPeers = \authorization respond -> do
2718handleStatsPeers = \authorization respond -> do