fix(cron): close sqlite connections deterministically in execution ledger - #69594
JoaoMarcos44 wants to merge 2 commits into
Conversation
|
suggesting changes The deterministic-close change works on this branch, but its current-main replay is broken. Current main added Please rebase and keep Security evidence:
Signed: GPT-5.6-sol-xhigh in Codex |
…, delegation, and verification ledgers Three durable ledgers used `with _connect() as conn:` where the sqlite3 connection context manager commits/rolls back but never closes, leaking the db/-wal/-shm file descriptors on every call. On a long-running gateway this exhausts RLIMIT_NOFILE and fails unrelated components with `[Errno 24] Too many open files`. Same bug class as the cron execution ledger (#69567 / PR #69594), which the connection helpers here are modeled on. Fix: route every ledger operation through a `_transaction()` context manager that guarantees `conn.close()` on exit. `_connect()` keeps its schema-on-connect contract (several tests call it directly) and now self-closes if schema init fails. Adds per-module regression tests asserting every opened connection is closed, including the no-op-update and exception-mid-transaction paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, delegation, and verification ledgers Three durable ledgers used `with _connect() as conn:` where the sqlite3 connection context manager commits/rolls back but never closes, leaking the db/-wal/-shm file descriptors on every call. On a long-running gateway this exhausts RLIMIT_NOFILE and fails unrelated components with `[Errno 24] Too many open files`. Same bug class as the cron execution ledger (#69567 / PR #69594), which the connection helpers here are modeled on. Fix: route every ledger operation through a `_transaction()` context manager that guarantees `conn.close()` on exit. `_connect()` keeps its schema-on-connect contract (several tests call it directly) and now self-closes if schema init fails. Adds per-module regression tests asserting every opened connection is closed, including the no-op-update and exception-mid-transaction paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…D exhaustion (NousResearch#69567) The cron execution ledger was leaking SQLite connections because `sqlite3.Connection` as a context manager only commits/rolls back -- it does NOT close the connection. Without explicit `conn.close()`, every ledger call (create, mark_running, finish, recover, list, latest) left an open connection and its WAL/SHM file descriptors. Created a `_transaction()` context manager that wraps lock acquisition, connection open, schema initialization, transaction commit/rollback, and deterministic connection close in a `finally` block. Schema init runs inside the `try` too so PRAGMA/DDL failures after a successful connect still close the connection. Key design: - `_connect()` now only opens the connection (no schema) - `_initialize_schema(conn)` handles PRAGMAs, DDL, and WAL setup - `_transaction()` combines lock + connect + init + commit/rollback + close - `apply_wal_with_fallback` is preserved (unlike PR NousResearch#69594 which replaced it with raw PRAGMA, losing NFS/SMB fallback) All 7 call sites migrated from `with _lock, _connect() as conn:` to `with _transaction() as conn:`. Added regression test `test_every_ledger_call_closes_sqlite_connection` that repeatedly calls all ledger functions and asserts the /proc/self/fd count for executions.db doesn't grow.
…, delegation, and verification ledgers Three durable ledgers used `with _connect() as conn:` where the sqlite3 connection context manager commits/rolls back but never closes, leaking the db/-wal/-shm file descriptors on every call. On a long-running gateway this exhausts RLIMIT_NOFILE and fails unrelated components with `[Errno 24] Too many open files`. Same bug class as the cron execution ledger (NousResearch#69567 / PR NousResearch#69594), which the connection helpers here are modeled on. Fix: route every ledger operation through a `_transaction()` context manager that guarantees `conn.close()` on exit. `_connect()` keeps its schema-on-connect contract (several tests call it directly) and now self-closes if schema init fails. Adds per-module regression tests asserting every opened connection is closed, including the no-op-update and exception-mid-transaction paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, delegation, and verification ledgers Three durable ledgers used `with _connect() as conn:` where the sqlite3 connection context manager commits/rolls back but never closes, leaking the db/-wal/-shm file descriptors on every call. On a long-running gateway this exhausts RLIMIT_NOFILE and fails unrelated components with `[Errno 24] Too many open files`. Same bug class as the cron execution ledger (NousResearch#69567 / PR NousResearch#69594), which the connection helpers here are modeled on. Fix: route every ledger operation through a `_transaction()` context manager that guarantees `conn.close()` on exit. `_connect()` keeps its schema-on-connect contract (several tests call it directly) and now self-closes if schema init fails. Adds per-module regression tests asserting every opened connection is closed, including the no-op-update and exception-mid-transaction paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, delegation, and verification ledgers Three durable ledgers used `with _connect() as conn:` where the sqlite3 connection context manager commits/rolls back but never closes, leaking the db/-wal/-shm file descriptors on every call. On a long-running gateway this exhausts RLIMIT_NOFILE and fails unrelated components with `[Errno 24] Too many open files`. Same bug class as the cron execution ledger (NousResearch#69567 / PR NousResearch#69594), which the connection helpers here are modeled on. Fix: route every ledger operation through a `_transaction()` context manager that guarantees `conn.close()` on exit. `_connect()` keeps its schema-on-connect contract (several tests call it directly) and now self-closes if schema init fails. Adds per-module regression tests asserting every opened connection is closed, including the no-op-update and exception-mid-transaction paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes #69567
🚨 The Problem (SQLite Connection & File Descriptor Leak)
The cron execution ledger (
cron/executions.py) opens SQLite connections on multiple operations during a normal job run cycle:create_execution()(connection 1)mark_execution_running()(connection 2)finish_execution()(connection 3)The code implicitly assumed that using
with _lock, _connect() as conn:would close the connection upon exiting the block. However, Python'ssqlite3.Connectionacts only as a transactional context manager (handlingcommit/rollbackon exit, but never closing the connection).In Linux environments running SQLite in WAL mode, each of these 3 unclosed connections keeps active file descriptors for
executions.dbandexecutions.db-wal(6 file descriptors per run). At a rate of ~60 cron executions per hour, the gateway process would hit the soft limit of 1,024 file descriptors (EMFILE) in less than 3 hours, leading to severe and cascading system-wide crashes (e.g. failing config writes, session database access, socket binds, etc.).🛡️ The Solution (Root Cause Fixed)
We centralized and decoupled connection lifecycle, transaction management, and locking into a dedicated context manager named
_transaction()in cron/executions.py:conn.close()is unconditionally called in afinallyblock._initialize_schema()) is now run inside thetry/finallyblock. If initialization fails (e.g., DDL or PRAGMA errors) after a successfulconnect(), the connection is still closed instead of leaked.with conn:) to ensure commits on success and rollbacks on exceptions.🧪 Automated Test Coverage
We added four new exhaustive and deterministic tests in test_execution_ledger.py to prevent regressions:
test_ledger_operations_close_every_connection: Tracks the connection lifecycle to verify that every single ledger call closes its connection.test_early_return_still_closes_connection: Ensures connections are closed even during premature early returns (such asmark_execution_runningexiting early on invalid status transitions).test_exception_during_operation_still_closes_connection: Verifies rollback followed by connection closing when statement execution fails.test_schema_init_failure_still_closes_connection: Verifies that a connection is closed if DDL schema creation raises an error.All 21 ledger tests are now passing successfully (
21 passed).🎨 Infographic: Cron Ledger FD Leak Safeguard