Skip to content

Lagged subscription-refresh monitor performs no catch-up when a relay flaps entirely within the dropped-notification window, silently losing that relay's gift wraps #327

Description

@erskingardner

Low-probability edge case: it requires the 64-slot relay monitor broadcast channel to overflow (RecvError::Lagged) and a relay to disconnect+reconnect entirely within the dropped-message window and no other relay to be concurrently down.

Summary

The Lagged arm of run_subscription_refresh_monitor is meant to be a conservative catch-up: when status notifications were dropped, it should re-request the gift-wrap subscription far enough back to cover whatever outage it may have missed. But its current logic assumes "no recorded disconnect ⇒ no gap occurred," which is false precisely in the case the lag handler exists to cover. When a relay disconnected and reconnected during the skipped-notification window, it performs no refresh at all, and gift wraps that relay would have delivered during its brief downtime are never re-requested.

Location

  • src/nostr/client.rs:256-310 — the Err(broadcast::error::RecvError::Lagged(skipped)) arm of run_subscription_refresh_monitor

Details

On a normal StatusChanged the monitor records each relay's disconnect time in disconnected_since and, on the matching Connected, refreshes since from that recorded time (lines 224-254). The Lagged arm tries to reconstruct an oldest_disconnect from:

  • currently-Connected relays that are still present in disconnected_since (lines 267-273), and
  • relays it can currently observe as down (lines 278-286).

Consider a relay that both dropped and came back while the channel was overflowed:

  1. Its Disconnected and Connected notifications are both among the skipped (dropped) messages, so it was never inserted into disconnected_since.
  2. In the Lagged scan it now reads as plainly Connected, so disconnected_since.remove(relay_url) returns None and it contributes nothing to refresh_from (lines 267-273).

If no other relay happens to be down at that moment, refresh_from stays None, disconnected_since is empty, oldest_disconnect is None (lines 290-293), and the if let Some(...) block (lines 295-309) is skipped entirely — no subscription refresh is issued. The flapped relay's outage window is silently un-caught and its gift wraps for that window are lost (they were only ever offered by that relay while it was reconnecting).

Concrete trigger: sustained relay status churn (many relays reconnecting at once, e.g. after a network blip across a large relay set) overflows the 64-slot RELAY_MONITOR_CHANNEL_SIZE; one relay's down→up pair lands in the dropped set.

A truly conservative handler cannot prove that no gap occurred when it has lost information, so it should refresh from a safe lower bound (process_started_at, or now - max_notification_age) whenever the lag arm fires and it cannot otherwise establish an older disconnect time — rather than doing nothing.

Why this is not a duplicate

Neither addresses the subscription-refresh monitor's Lagged arm producing no refresh at all when the only disconnect it needed to catch was itself among the dropped notifications.

Impact

Silent, unrecoverable loss of a subset of gift wraps (missed silent push notifications) after a burst of relay status churn. Bounded to what a single flapped relay uniquely held during a sub-second-to-seconds reconnect, and gated on the rare Lagged + flap-in-window coincidence — hence LOW.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions