Files
amethyst/commons/src/jvmTest
nrobi144andClaude Opus 4.7 96371715f4 feat(commons): per-relay latency tracker + slow-relay classifier
Phase 1 of the desktop relay-latency-health feature: add a rolling-window
latency tracker that decorates the quartz RelayConnectionListener, plus a
pure classifier that flags relays whose per-metric p50 exceeds 2× the cohort
median. No store integration or UI yet — those come in follow-up commits.

commons commonMain (CLI-safe, no Compose runtime, no JVM-only deps):
  - LatencyMetric: OK_ACK / EOSE / FIRST_RESULT / PING
  - MetricSample: @Immutable (p50Ms, count)
  - RelayLatencySnapshot: @Immutable, backed by ImmutableMap so strong
    skipping engages when unchanged rows are re-emitted
  - SlowReason: @Immutable (metric, relayP50, cohortP50, multiplier)
  - HealthReason sealed interface: Unresponsive(gap) | Slow(SlowReason)
  - classifySlowRelays(): pure. Honors NIP-11 auth_required /
    payment_required (paid/auth-only relays are excluded from both cohort
    and target until auth completes — otherwise they'd be perpetually
    flagged while CLOSED'ing anonymous queries).

commons jvmAndroid (ConcurrentHashMap is JVM-only):
  - LatencyRingBuffer: fixed-capacity (default 50) IntArray ring,
    synchronized push, snapshotMedian / snapshotSamples / restore.
  - RelayLatencyTracker: pending-eventId / pending-subId / firstResultSeen
    maps + per-(relay, metric) ring buffers. Handles every pairing rule
    the deepened plan called out:
      * onSent EventCmd → record eventId timestamp
      * onSent ReqCmd   → record subId timestamp; clear firstResultSeen
      * onSent CloseCmd → drop pending subId (no sample) — prevents
        ComposeSubscriptionManager's sub-id reuse from pairing late
        events with a new REQ
      * success=false   → no-op (websocket buffer was full)
      * OkMessage       → pair by eventId, push OK_ACK
      * EventMessage    → first-only, push FIRST_RESULT
      * EoseMessage     → pair by subId, push EOSE
      * ClosedMessage   → drop pending (fast negative response, not a
        latency signal — was previously recording 300s TTL samples for
        any auth-required relay)
      * onConnected     → push PING
      * onDisconnected  → drop all pending (no TTL samples)
      * sweep(now)      → TTL-expire pending entries (60s OK / 300s REQ),
        record TTL value as the sample
    AUTH retries: the second onSent overwrites the timestamp, so samples
    reflect the retry leg — matches the user's mental model of "speed of
    the actual publish". Pending maps are size-capped at 256 entries per
    relay as a safety net against adversarial relays. Tracker owns no
    CoroutineScope — RelayHealthStore drives sweep + snapshot from its
    existing 60s reclassify tick (Phase 2).
  - RelayLatencyListener: thin RelayConnectionListener decorator,
    installInto / uninstallFrom paralleling RelayHealthListener.

Tests:
  - LatencyRingBufferTest (7): wrap, median odd/even, restore from larger
    or smaller arrays, chronological snapshotSamples.
  - RelayLatencyTrackerTest (13): OK pairing, EOSE + FIRST_RESULT pairing,
    success=false ignore, CloseCmd drops pending, ClosedMessage drops
    pending, AUTH retry overwrites timestamp, disconnect drops all,
    sweep TTL semantics (OK vs REQ), FIRST_RESULT only sampled when not
    yet seen, ping, per-relay isolation, 256-entry cap, restore
    round-trip.
  - ClassifySlowRelaysTest (9): empty, Tor short-circuit, cohort < 2,
    2× flag, count-below-min excludes from cohort, NIP-11 auth_required
    excludes / includes once auth complete, payment_required excludes,
    worst-metric-multiplier wins when multiple flag, exact-2× does not
    flag (strict greater-than).

Note: a pre-existing RelayHealthStoreCloseTest case on the base branch
(fix/relay-health-threading-and-sleep-resume) hangs in advanceUntilIdle.
Not related to this commit; new tests pass cleanly with a tighter test
filter. Will revisit when integrating Phase 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 12:11:25 +03:00
..