Skip to content

PushNotificationManager

Fans out symbol configuration changes to all PriceCollector and PriceAlert Durable Objects across all geo-locations.

High-Level Design

What It Does

When symbols are enabled/disabled in the SymbolRegistry, every downstream DO (PriceCollector, PriceAlert) needs the updated configuration. PushNotificationManager owns this fan-out — pushing to all 77 DOs in production (7 PriceCollectors + 70 PriceAlerts across 7 geo-locations).

Data Flow

Admin enables/disables symbol

SymbolStorageManager updates SQLite

PushNotificationManager.pushWithRetryOnFailure()

    ┌───────────────────────────────────────────┐
    │ Query enabled symbols from `symbols` table │
    └───────────────────────────────────────────┘
        ↓                           ↓
  7 PriceCollectors            70 PriceAlerts
  (full metadata:              (symbol strings
   symbol, ticker,              only, for
   decimals, priceScale)        validation)

All 77 pushes run in parallel via Promise.allSettled — a single DO failure never blocks the other 76.

Differentiated Payloads

PriceCollectors need full EnabledSymbolMetadata[] (symbol, ticker, decimals, priceScale) for price transformation and decimal handling. PriceAlerts only need string[] of symbol names for alert validation. This keeps the PriceAlert payload minimal since it doesn't perform price transformation.

Retry & Reconciliation Strategy

The manager has two layers of consistency guarantees:

  1. Immediate retry with exponential backoff — on any push failure, schedules an alarm at 30s → 1m → 2m → 5m → 15m (capped). Won't override an existing sooner alarm to avoid churn.

  2. Periodic reconciliation — re-pushes current state regardless of whether changes occurred. Cadence depends on crawler config: when the crawler is enabled, reconciliation piggybacks on the crawler alarm (default 1h); when disabled, a standalone 15-min interval is used. Acts as a safety net for: network blips, DO evictions that lost in-memory config, or retries that exhausted backoff.

The consecutiveFailures counter is intentionally in-memory only. On DO eviction it resets to 0, which is fine — the next reconciliation alarm will re-push the full state anyway.

Fail-Open Downstream Behavior

Downstream DOs are designed to degrade gracefully if they haven't received a push yet. Both persist received symbols to local KV storage, so a DO eviction recovers from KV on boot before falling back to SymbolRegistry RPC. If neither source is available:

  • PriceCollector falls back to the static HyperliquidSymbolInfo mapping
  • PriceAlert allows all symbols (null set = no filtering)

This means the system never hard-fails from a missed push — it operates with progressively less specific config (KV cache → static defaults) until the next successful push.

Idempotency

Pushes are fully idempotent. Pushing the same enabled symbol list multiple times is a no-op on the receiving DOs. This makes reconciliation and retry safe without deduplication logic.

Edge Cases & Error Handling

  • Individual DO failures do not block other pushes (Promise.allSettled).
  • Exponential backoff caps at 15 minutes to avoid indefinite delays.
  • Will not override an existing alarm that fires sooner than the proposed retry.
  • Reconciliation alarm is a no-op if any alarm is already scheduled (prevents alarm churn).
  • Empty enabled symbols list is rejected by downstream DOs (they log an error and skip the update). This prevents accidental disabling of all symbols from a transient query issue.

See Also

  • SymbolStorageManager - owns the symbols table queried for metadata
  • CrawlerConfigManager - enabled flag determines if reconciliation runs with crawl
  • Parent DO: SymbolRegistry
  • Downstream DOs: PriceCollector, PriceAlert