Skip to content

Commit 5d10e31

Browse files
chore: corruption-hardening ops (shutdown budget, journald logs, DuckDB spill)
Follow-up to the durable derivation queue. Three small prod-hardening changes against the 2026-06-06 incident's failure modes: 1. Shutdown budget made coherent: split into SHUTDOWN_WORKER_WAIT (30s, lets the worker finish its current derive) + SHUTDOWN_WRITE_WAIT (10s, in-flight raw upsert), summing to 40s < the new 45s stop_grace_period. Previously both phases used 25s each (worst case 50s > 30s grace -> Docker could SIGKILL mid-write). 2. API logging switched json-file -> journald so logs land in the host journal and SURVIVE container recreation. Root cause was unprovable on 2026-06-06 because the dead container's json-file logs vanished when it was recreated. docker logs + journalctl -t ohlc-datalake-api both still work. 3. DuckDB temp_directory set on the data volume so operations spill to disk past the 2GB memory_limit instead of growing until the cgroup OOM-kills the container mid-write (OOM-mid-checkpoint is the leading incident suspect on the 7.6GiB host). Full suite 247 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c7196b1 commit 5d10e31

3 files changed

Lines changed: 29 additions & 8 deletions

File tree

docker-compose.prod.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,18 @@ services:
5959
memory: 4g
6060
cpus: "2.0"
6161
restart: unless-stopped
62-
# Let the FastAPI shutdown hook drain in-flight DuckDB writes before Docker SIGKILLs.
63-
stop_grace_period: 30s
62+
# Let the FastAPI shutdown hook drain the derivation worker + in-flight DuckDB
63+
# writes before Docker SIGKILLs. Must exceed api.py's SHUTDOWN_WORKER_WAIT +
64+
# SHUTDOWN_WRITE_WAIT budget (30 + 10 = 40s) — see that file.
65+
stop_grace_period: 45s
66+
# journald (not json-file) so the API's logs land in the host system journal
67+
# and SURVIVE container recreation. After the 2026-06-06 incident the dead
68+
# container's logs were gone (json-file dies with the container), making the
69+
# crash unprovable. `docker logs` still works; so does `journalctl -t ohlc-datalake-api`.
6470
logging:
65-
driver: json-file
71+
driver: journald
6672
options:
67-
max-size: "50m"
68-
max-file: "5"
73+
tag: "ohlc-datalake-api"
6974
networks:
7075
- datalake-network
7176

src/api.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@
1717
from src.config import validate_secrets
1818
from src.auth.auth import ScopedAuth
1919

20-
SHUTDOWN_WRITE_WAIT_SECONDS = 25.0
20+
# Shutdown budget, split in two phases. The SUM must stay below the
21+
# docker-compose `stop_grace_period` (45s) or Docker SIGKILLs us mid-write — the
22+
# exact kill-during-checkpoint that risks DuckDB corruption. Phase 1 lets the
23+
# derivation worker finish its current task; phase 2 then waits out any in-flight
24+
# ingest's (short) raw upsert. 30 + 10 = 40s < 45s, leaving margin.
25+
SHUTDOWN_WORKER_WAIT_SECONDS = float(os.getenv("SHUTDOWN_WORKER_WAIT_SECONDS", "30"))
26+
SHUTDOWN_WRITE_WAIT_SECONDS = float(os.getenv("SHUTDOWN_WRITE_WAIT_SECONDS", "10"))
2127
from src.routes import (
2228
catalog_router,
2329
instruments_router,
@@ -96,8 +102,9 @@ def startup_event():
96102
@app.on_event("shutdown")
97103
def shutdown_event():
98104
"""Stop the derivation worker, then block until in-flight writes finish so SIGTERM can't interrupt a transaction."""
99-
# Stop the worker first so it can't start a NEW derive while we're draining.
100-
derivation_queue.stop_worker(timeout=SHUTDOWN_WRITE_WAIT_SECONDS)
105+
# Stop the worker first so it can't start a NEW derive while we're draining;
106+
# this also waits out the worker's current derive task (phase 1 of the budget).
107+
derivation_queue.stop_worker(timeout=SHUTDOWN_WORKER_WAIT_SECONDS)
101108
logger.info("Shutdown: waiting for in-flight writes", extra={"timeout_s": SHUTDOWN_WRITE_WAIT_SECONDS})
102109
acquired = _write_tx_lock.acquire(timeout=SHUTDOWN_WRITE_WAIT_SECONDS)
103110
if acquired:

src/core/datalake.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,18 @@ def _get_shared_connection() -> duckdb.DuckDBPyConnection:
3434
if _db_connection is None:
3535
_db_connection = duckdb.connect(str(DUCKDB_PATH))
3636
_db_connection.execute(f"SET memory_limit = '{DUCKDB_MEMORY_LIMIT}'")
37+
# Spill to disk on the data volume when an operation exceeds the
38+
# memory_limit, instead of letting the process grow until the
39+
# cgroup OOM-kills the container mid-write (a corruption risk, and
40+
# the leading suspect for the 2026-06-06 incident on the RAM-tight
41+
# host). The data volume has ample disk vs. the small memory cap.
42+
tmp_dir = DUCKDB_PATH.parent / "duckdb_tmp"
43+
tmp_dir.mkdir(parents=True, exist_ok=True)
44+
_db_connection.execute(f"SET temp_directory = '{tmp_dir.as_posix()}'")
3745
logger.info("DuckDB connection opened", extra={
3846
"path": str(DUCKDB_PATH),
3947
"memory_limit": DUCKDB_MEMORY_LIMIT,
48+
"temp_directory": str(tmp_dir),
4049
})
4150
return _db_connection
4251

0 commit comments

Comments
 (0)