How apps/db turns feed bytes into normalized rows and analytical tables, and the invariants each stage keeps.
| Reference | Value |
|---|---|
| Subject and boundary | Static pipeline, realtime cycle, Gold delay and service computations, and pruning invariants. Publication is on Snapshot publication and public contract; tables on ‣. |
| Verified source | orchestration.py, ingestion/, silver/, gold/, worker_starts.py, realtime_schedule.py, worker_watchdog.py, live_policy.py at main 9733eb26d92533f56cdc8bc46b81b93fbb5987c8, checked November 10, 2026. |
| Support state | Implemented source. Production schedule and services are on ‣. |
flowchart LR
F[Provider feed] -->|download + SHA-256| B[(Bronze object<br/>raw.ingestion_runs)]
B --> S[Silver<br/>normalized rows]
S --> G[Gold facts<br/>and serving state]
G --> R[5-minute and hourly<br/>delay summaries]
R --> D[Daily warm rollups]
G --> L[Live publication]
D --> H[Historic publication]
| Stage | Owner | Key invariant |
|---|---|---|
| Capture | ingestion/ |
A capture succeeds only after its Bronze object and database lineage are stored. SHA-256 is computed while streaming to a temporary file; a write or close failure prevents a successful artifact |
| Failed capture cleanup | ingestion.common.finish_failed_capture |
Recording the failed run, removing the temporary file and deleting an orphaned upload are independent best-effort steps; the original exception is preserved and messages are redacted |
| Silver | silver/ |
Static, realtime, GIS and i3 alerts normalize separately. Silver and Gold commit in separate transactions |
| Gold | gold/ |
Facts, serving caches, markers and rollup invalidation commit together |
| Rollups | gold/rollups.py, gold/delay_*.py |
Each kind keeps its own watermark; dirty days are refused, not silently rebuilt |
run-static-pipeline <provider> downloads the static GTFS (and STM GIS), records the bytes in Bronze, loads Silver and refreshes Gold.
dataset_version_id stays null until Silver loads.run-realtime-worker <provider> first records its start in core.worker_starts through a recorder subprocess bounded to five seconds; a recorder failure logs one warning and the loop starts anyway, and a shutdown signal during that wait exits before the first cycle. It then runs the realtime cycle in process (orchestration.py) on each worker tick; run-realtime-cycle is the matching one-shot CLI command. One worker process runs per provider.
| Provider | Endpoint | Interval | Phase |
|---|---|---|---|
| STM | vehicle_positions |
20 s | refresh_phase_seconds: 10: captures at :10, :30 and :50, two seconds after STM rebuilds the feed |
| STM | trip_updates |
30 s | None |
| STM | i3_alerts |
300 s | None |
| OC Transpo | vehicle_positions |
20 s | None; capture is unphased |
| OC Transpo | trip_updates |
30 s | None. No alerts feed is configured |
Cadence sources, updated by #746: stm.yaml, octranspo.yaml, realtime_schedule.py. The OC worker was rolled out on November 10, 2026 8:53 PM; production evidence is in ‣.
realtime_schedule.py owns the gating:
worker_tick_seconds is the greatest common divisor of REALTIME_POLL_SECONDS, every enabled realtime feed's interval, and any declared phase. Static/GIS and disabled feeds do not shorten the tick. The current configuration gives a 10 s tick for both STM and OC Transpo. A provider with a phased feed sleeps to tick boundaries aligned to the UTC epoch; unphased OC keeps a start-to-start cadence and has no artificial two-second source-phase capture window.endpoint_due skips an endpoint until its interval minus a 2-second tolerance (CAPTURE_CADENCE_TOLERANCE_SECONDS) has passed since its last capture or attempt. A phased endpoint also runs only within 2 seconds after its slot and once per slot; a missed slot waits for the next one.