Appearance
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:
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.
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
HyperliquidSymbolInfomapping - 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
symbolstable queried for metadata - CrawlerConfigManager -
enabledflag determines if reconciliation runs with crawl - Parent DO:
SymbolRegistry - Downstream DOs:
PriceCollector,PriceAlert