From e33cccfdded842dd33e002468b95d59c25dc8667 Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 18 Jul 2026 21:17:22 +0000 Subject: [PATCH] fix(db): close SQLite connections explicitly to suppress ResourceWarning on Python 3.13 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sqlite3.Connection.__exit__` only commits/rolls back a transaction — it does not close the connection. Python 3.13 (the Nix env on the KVM runner) emits `ResourceWarning: unclosed database` for every connection GC'd without an explicit close, producing noisy output in the coverage job. Add `DbStore._connection()`, a `contextmanager` that calls `self._connect()`, wraps it in the existing transaction context manager, and closes the connection in a `finally` block. Change all `with self._connect() as conn:` call sites in `db_store.py`, `audit_store.py`, `queue_store.py`, and `orchestrator/registry.py` to `with self._connection() as conn:`. `_connect()` remains as the per-subclass hook (RegistryStore overrides it to set `busy_timeout`); `_connection()` delegates to `self._connect()` so the override is respected. --- bot_bottle/audit_store.py | 4 ++-- bot_bottle/db_store.py | 14 ++++++++++++-- bot_bottle/orchestrator/registry.py | 12 ++++++------ bot_bottle/queue_store.py | 14 +++++++------- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/bot_bottle/audit_store.py b/bot_bottle/audit_store.py index c01d2bb..2f48f7c 100644 --- a/bot_bottle/audit_store.py +++ b/bot_bottle/audit_store.py @@ -42,7 +42,7 @@ class AuditStore(DbStore): super().__init__(db_path or host_db_path(), migrations) def write_audit_entry(self, entry: AuditEntry) -> Path: - with self._connect() as conn: + with self._connection() as conn: conn.execute( """ INSERT INTO supervise_audit_entries ( @@ -66,7 +66,7 @@ class AuditStore(DbStore): def read_audit_entries(self, component: str, slug: str) -> list[AuditEntry]: if not self.db_path.is_file(): return [] - with self._connect() as conn: + with self._connection() as conn: rows = conn.execute( """ SELECT * FROM supervise_audit_entries diff --git a/bot_bottle/db_store.py b/bot_bottle/db_store.py index 5d4bf37..256f29e 100644 --- a/bot_bottle/db_store.py +++ b/bot_bottle/db_store.py @@ -3,6 +3,7 @@ from __future__ import annotations import sqlite3 +from contextlib import contextmanager from pathlib import Path try: @@ -28,12 +29,21 @@ class DbStore: conn.row_factory = sqlite3.Row return conn + @contextmanager + def _connection(self): + conn = self._connect() + try: + with conn: + yield conn + finally: + conn.close() + def is_migrated(self) -> bool: """Return True if the DB is fully up-to-date, False if migration is needed.""" if not self.db_path.exists(): return False try: - with self._connect() as conn: + with self._connection() as conn: row = conn.execute( "SELECT version FROM schema_versions WHERE module = ?", (self._migrations.schema_key,), @@ -45,7 +55,7 @@ class DbStore: def migrate(self) -> None: """Apply any pending migrations and set permissions on the DB file.""" - with self._connect() as conn: + with self._connection() as conn: self._migrations.apply(conn) self._chmod() diff --git a/bot_bottle/orchestrator/registry.py b/bot_bottle/orchestrator/registry.py index dc200a3..1b9b38b 100644 --- a/bot_bottle/orchestrator/registry.py +++ b/bot_bottle/orchestrator/registry.py @@ -167,7 +167,7 @@ class RegistryStore(DbStore): metadata=metadata, policy=policy, ) - with self._connect() as conn: + with self._connection() as conn: conn.execute( "DELETE FROM orchestrator_bottles " "WHERE source_ip = ? AND state = 'active' AND bottle_id != ?", @@ -193,7 +193,7 @@ class RegistryStore(DbStore): def set_policy(self, bottle_id: str, policy: str) -> bool: """Update a bottle's policy in place (live reload). Returns True if the bottle exists.""" - with self._connect() as conn: + with self._connection() as conn: cur = conn.execute( "UPDATE orchestrator_bottles SET policy = ? WHERE bottle_id = ?", (policy, bottle_id), @@ -203,7 +203,7 @@ class RegistryStore(DbStore): def deregister(self, bottle_id: str) -> bool: """Remove a bottle. Returns True if a row was deleted.""" - with self._connect() as conn: + with self._connection() as conn: cur = conn.execute( "DELETE FROM orchestrator_bottles WHERE bottle_id = ?", (bottle_id,) ) @@ -211,7 +211,7 @@ class RegistryStore(DbStore): def get(self, bottle_id: str) -> BottleRecord | None: """Return the bottle by id, or None if absent.""" - with self._connect() as conn: + with self._connection() as conn: row = conn.execute( "SELECT * FROM orchestrator_bottles WHERE bottle_id = ?", (bottle_id,) ).fetchone() @@ -219,7 +219,7 @@ class RegistryStore(DbStore): def all(self) -> list[BottleRecord]: """Every registered bottle, oldest first.""" - with self._connect() as conn: + with self._connection() as conn: rows = conn.execute( "SELECT * FROM orchestrator_bottles ORDER BY created_at" ).fetchall() @@ -232,7 +232,7 @@ class RegistryStore(DbStore): source IP is unspoofable (Firecracker `/31` + nft) and the control plane is reachable only by the trusted gateway; pair with the identity token (`attribute`) elsewhere.""" - with self._connect() as conn: + with self._connection() as conn: rows = conn.execute( "SELECT * FROM orchestrator_bottles " "WHERE source_ip = ? AND state = 'active'", diff --git a/bot_bottle/queue_store.py b/bot_bottle/queue_store.py index 51072c0..b547d90 100644 --- a/bot_bottle/queue_store.py +++ b/bot_bottle/queue_store.py @@ -66,7 +66,7 @@ class QueueStore(DbStore): super().__init__(resolved, migrations) def write_proposal(self, proposal: Proposal) -> Path: - with self._connect() as conn: + with self._connection() as conn: conn.execute( """ INSERT OR REPLACE INTO supervise_proposals ( @@ -89,7 +89,7 @@ class QueueStore(DbStore): return self.db_path def read_proposal(self, proposal_id: str) -> Proposal: - with self._connect() as conn: + with self._connection() as conn: row = conn.execute( """ SELECT * FROM supervise_proposals @@ -104,7 +104,7 @@ class QueueStore(DbStore): def list_pending_proposals(self) -> list[Proposal]: if not self.db_path.is_file(): return [] - with self._connect() as conn: + with self._connection() as conn: rows = conn.execute( """ SELECT p.* FROM supervise_proposals p @@ -125,7 +125,7 @@ class QueueStore(DbStore): def list_all_pending_proposals(self) -> list[Proposal]: if not self.db_path.is_file(): return [] - with self._connect() as conn: + with self._connection() as conn: rows = conn.execute( """ SELECT p.* FROM supervise_proposals p @@ -142,7 +142,7 @@ class QueueStore(DbStore): return [self._row_to_proposal(row) for row in rows] def write_response(self, response: Response) -> Path: - with self._connect() as conn: + with self._connection() as conn: conn.execute( """ INSERT OR REPLACE INTO supervise_responses ( @@ -161,7 +161,7 @@ class QueueStore(DbStore): return self.db_path def read_response(self, proposal_id: str) -> Response: - with self._connect() as conn: + with self._connection() as conn: row = conn.execute( """ SELECT * FROM supervise_responses @@ -176,7 +176,7 @@ class QueueStore(DbStore): def archive_proposal(self, proposal_id: str) -> None: if not self.db_path.is_file(): return - with self._connect() as conn: + with self._connection() as conn: conn.execute( """ UPDATE supervise_proposals SET archived = 1