3.2 KiB
Simplex.Messaging.Notifications.Server.Stats
NTF router statistics collection with own-router breakdown and backward-compatible persistence.
Source: Notifications/Server/Stats.hs
Non-obvious behavior
1. incServerStat double lookup
incServerStat performs a non-STM IO lookup first. On cache hit, the STM transaction only touches the per-router TVar Int without reading the shared TMap, avoiding contention. On cache miss, the STM block re-checks the map to handle races (another thread may have inserted between the IO lookup and STM entry).
2. setNtfServerStats is not thread safe
setNtfServerStats is explicitly documented as non-thread-safe and intended for router startup only (restoring from backup file).
3. Backward-compatible parsing
The strP parser uses opt which defaults missing fields to 0. This allows reading stats files from older router versions that don't include newer fields (ntfReceivedAuth, ntfFailed, ntfVrf*, etc.).
4. getNtfServerStatsData is a non-atomic snapshot
getNtfServerStatsData reads each IORef and TMap field sequentially in plain IO, not inside a single STM transaction. The returned NtfServerStatsData is not a consistent point-in-time snapshot — invariants like "received >= delivered" may not hold. The same applies to getStatsByServer, which does one readTVarIO for the map root TVar, then a separate readTVarIO for each per-router TVar. This is acceptable for periodic reporting where approximate consistency suffices.
5. Mixed IORef/TVar concurrency primitives
Aggregate counters (ntfReceived, ntfDelivered, etc.) use IORef Int incremented via atomicModifyIORef'_, while per-router breakdowns use TMap Text (TVar Int) incremented atomically via STM in incServerStat. Although both individual operations are atomic, the aggregate and per-router increments are separate operations, so their values can drift: a thread could increment the aggregate IORef before incServerStat runs, or vice versa.
6. setStatsByServer replaces TMap atomically but orphans old TVars
setStatsByServer builds a fresh Map Text (TVar Int) in IO via newTVarIO, then atomically replaces the TMap's root TVar. Old per-router TVars are not reused — any other thread holding a reference from a prior TM.lookupIO would modify an orphaned counter. Safe only because it's called at startup (like setNtfServerStats), but lacks the explicit "not thread safe" comment.
7. Positional parser format despite key=value appearance
The parser is strictly positional: fields must appear in exactly the serialization order. The opt alternatives only handle entirely absent fields (defaulting to 0), not reordered fields. Despite the key=value on-disk appearance, this is a sequential format — the named prefixes are for human readability, not key-lookup parsing.
8. B.unlines trailing newline asymmetry
strEncode uses B.unlines, which appends \n after every element including the last. The parser compensates with optional A.endOfLine on the last field. The file always ends with \n, but the parser tolerates its absence.