Appearance
AlertCursorManager
Manages cursor-based iteration state for the PriceAlert alarm loop. Tracks which second of price data has been processed, enabling reliable catch-up after DO evictions.
High-Level Design
Core Concept: The Cursor
The PriceAlert alarm fires every 1 second. Each second of price data has a unix timestamp. The cursor is the last timestamp that was successfully processed. On each alarm:
cursor = 1705000010 (last processed)
now = 1705000013
→ Process: 1705000011, 1705000012, 1705000013
→ Advance cursor to 1705000013This gives the alarm loop exactly-once semantics — it always knows where to resume, even after eviction.
Data Flow
alarm() fires
│
▼
cursorManager.getCursorRange(now)
│ returns { startCursor: cursor+1, endCursor: now }
▼
alarm() caps endCursor to startCursor + 3599 ◄── Unbounded catch-up protection
│
▼
Fetch prices for [startCursor..endCursor] from PriceDataManager
│
▼
For each timestamp in range:
├─ Has price data? → evaluate alerts, advance lastProcessedTs
├─ Empty but finalized? → skip, notify arrival alerts, advance
└─ Not finalized? → STOP (don't advance cursor past this point)
│
▼
cursorManager.updateCursor(lastProcessedTs, triggeredCount)
│ persists to SQLite + updates in-memory state
▼
Next alarm picks up from lastProcessedTs + 1Key Design Decisions
Safe cursor advancement. The cursor only advances to the last timestamp where data was successfully processed. If timestamp T has no data and isn't finalized (backfill still in progress), the loop breaks and the cursor stays at T-1. The next alarm retries from T.
Catch-up window cap. After a long eviction (e.g., 2 hours), processing all missed seconds in one alarm would spike CPU. The MAX_CATCHUP_WINDOW = 3600 (1 hour) caps the range per alarm cycle. Remaining seconds are caught up in subsequent alarms.
Finalized-empty gap handling. PriceCollector reports a finalizedUpTo watermark. If a timestamp is before this watermark (ts < finalizedUpTo) but has no data, it means data will never arrive (interpolation exhausted). The cursor skips it but still fires price arrival alerts with empty data so pending market orders fail gracefully instead of staying orphaned.
Always fetch prices. The alarm fetches prices even when no alerts exist. This populates the PriceDataManager's in-memory cache, which serves as a read replica for UserPortfolio price queries.
State & Storage
In-Memory State
| Property | Type | Lifecycle |
|---|---|---|
lastCheckedTimestamp | number | Loaded from SQLite on initialize(), updated on each updateCursor() call. |
SQLite Tables
alert_metrics (singleton row, id = 1)
| Column | Type | Purpose |
|---|---|---|
total_alerts | integer | Cumulative alerts registered |
alerts_triggered_today | integer | Rolling triggered count |
last_checked_timestamp | integer | Cursor position (unix seconds) |
On initialize(), the in-memory value is restored from SQLite. On first-ever run, cursor seeds to now - 1 to avoid processing unbounded history.
Interactions
- PriceAlert alarm loop — sole consumer. Calls
getCursorRange()→ processes data → callsupdateCursor(). - PriceAlert health endpoint — calls
getHealthStatus()for monitoring dashboard metrics (flushed to Analytics Engine every 60s).
Edge Cases
- First-ever initialization seeds cursor to
now - 1to avoid processing unbounded history. - No timestamps processed — if the first timestamp in the range has no data and isn't finalized,
lastProcessedTsremains atstartCursor - 1(the current cursor), soupdateCursor()is called but does not advance. getCursorRangereturnstimestampsToCheck <= 0when called faster than once per second; callers must handle this.- Staleness threshold of 5 seconds means brief DO evictions are tolerated without marking unhealthy.
See Also
- AlertEvaluationManager — evaluates alerts within the cursor range
- PriceDataManager — fetches price data for the cursor range
- AlertStorageManager — provides alerts to evaluate