Skip to content

Data flow & Redis

The engine and the API never call each other directly. They coordinate entirely through Redis: the engine maintains status and health keys and consumes commands; the API reads those keys and publishes commands. This page is the map of that shared state.

The corporate-announcements pipeline

sequenceDiagram
    participant Poller as corp_ann poller
    participant Redis
    participant Worker as processor worker
    participant NSE
    participant LLM
    participant PG as PostgreSQL

    loop every ~5s
        Poller->>NSE: GET /corporate-announcements
        NSE-->>Poller: items[]
        loop each item
            Poller->>Redis: SET inflight:corp_ann:{seq_id} NX EX 3600
            Note right of Redis: skip if already inflight
            Poller->>Redis: RPUSH queue:corp_ann {item}
        end
    end

    Worker->>Redis: BLPOP queue:corp_ann
    Worker->>Redis: SET dedup:corp_ann:{seq_id} NX EX 172800
    Note right of Redis: skip if duplicate
    Worker->>NSE: GET attachment PDF
    Worker->>LLM: summarise + classify (multimodal)
    Worker->>PG: upsert Announcement
    Worker->>Redis: SET result:{date}:{symbol}:{seq_id}
    Worker->>Redis: PUBLISH alerts:{symbol}

Redis key map

All keys are defined in database/redis.py.

Work queue & deduplication

Key Type Set by TTL Purpose
queue:{api} list poller (RPUSH) → worker (BLPOP) Per-stream work queue.
inflight:{api}:{item_id} string poller 1 h Guard so the same item isn't enqueued twice while in flight.
dedup:{api}:{seq_id} string processor 48 h Guard so an item isn't processed twice.

Results & delivery

Key Type Set by TTL Purpose
result:{YYYYMMDD}:{symbol}:{seq_id} string (JSON) processor until midnight IST Daily cache of a processed announcement payload.
alerts:{symbol} pub/sub channel processor (PUBLISH) Live alert stream that delivery adapters subscribe to.
watch:{symbol} API Watchers of a symbol.
user:{user_id}:channels API A user's delivery channels.

Poller health (written by the engine, read by the API)

Key Type Purpose
poller:{api}:heartbeat string (epoch) Liveness beacon; TTL = 3 × interval. Absence ⇒ stalled.
poller:{api}:last_success string (epoch) Last time the poller fetched data.
poller:{api}:status string running · paused · backing_off · circuit_open.
poller:{api}:error_count string (int) Consecutive failures.
poller:{api}:interval string (float) Current poll interval (grows while backing off).
processor:{api}:status string running · paused.

Events & control

Key Type Purpose
engine:events list (capped at 200) Rolling event log; newest first. Written with push_event, read with read_events.
engine:control pub/sub channel Commands from the API to the engine's supervisor.

Deduplication and reprocessing

MarkAnn uses two guards so items are neither queued twice nor processed twice, while still being retried after a transient failure:

  • The poller sets inflight:{api}:{item_id} (1 h) before enqueuing. NSE returns the same announcements on every poll; this stops the same item flooding the queue.
  • The processor claims dedup:{api}:{seq_id} (48 h) before processing. This stops two workers analysing the same item.

On a processing failure, the processor releases both guards so the item is eligible to be re-enqueued and reprocessed on the next poll — with one exception: on LLMRateLimitError it releases only dedup, because the ConsumerPool re-queues the item itself and the inflight guard must stay to prevent a duplicate enqueue.

Why this matters

Before this behaviour existed, a failed item released only its dedup key. The inflight guard lingered for its full 1 h TTL, blocking the poller from re-enqueuing — so an item that failed because (say) the LLM provider was briefly down could sit stranded for up to an hour even after recovery. Releasing both keys on failure means a recovered provider reprocesses the backlog on the very next run. See the runbook.

Control command flow

sequenceDiagram
    participant UI as Admin console
    participant GW as Gateway
    participant API as Backend
    participant Redis
    participant ENG as Engine supervisor

    UI->>GW: POST /admin/processors/corp_ann/pause
    GW->>API: proxy (+ x-user-role)
    API->>Redis: PUBLISH engine:control {component, action}
    Redis-->>ENG: message
    ENG->>ENG: supervisor.pause("processor:corp_ann")
    ENG->>Redis: SET processor:corp_ann:status paused