Skip to content

Commit 69ef216

Browse files
committed
Dispose DB engine in CLI to fix Python 3.13 ResourceWarning leak
The `rcdb run` CLI test test_number_shortcut_equals_run (added in 876e2a5) fails only on Python 3.13: it asserts that `rcdb 1000` and `rcdb run 1000` produce identical captured output, but Python 3.13 emits `ResourceWarning: unclosed database` for the leaked SQLite connection at GC time, and Click's CliRunner folds stderr into the captured output, so the warning non-deterministically pollutes one of the two runs. Root cause: the CLI never disconnected its lazily-created RCDBProvider, and `disconnect()` only closed the ORM session, never disposing the engine - so the pooled DBAPI connection stayed open until garbage collection. - provider.disconnect(): also dispose the engine so the connection pool releases its DBAPI connections (null-safe). - app_context: add close() to tear down the provider. - cli/app: register ctx.call_on_close(close) so every command disposes the engine deterministically instead of relying on GC. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SeseguLYWWEBNUZc2Fq4j9
1 parent 9c202a2 commit 69ef216

3 files changed

Lines changed: 30 additions & 2 deletions

File tree

python/rcdb/app_context.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ def db(self) -> RCDBProvider:
1919
self._db_instance = RCDBProvider(self.connection_str)
2020
return self._db_instance
2121

22+
def close(self):
23+
"""Disconnect the database if one was opened.
24+
25+
Safe to call when no connection was ever made. The CLI registers this on
26+
the Click context so the engine is disposed deterministically when the
27+
command finishes, instead of waiting for garbage collection (which on
28+
Python 3.13+ leaks a ``ResourceWarning: unclosed database``).
29+
"""
30+
if self._db_instance is not None:
31+
self._db_instance.disconnect()
32+
self._db_instance = None
33+
2234
def require_connected_db(self) -> RCDBProvider:
2335
"""Return the RCDBProvider, or fail with a clear CLI error if no connection was given.
2436

python/rcdb/cli/app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ def rcdb_cli(ctx, user_config, connection, config, verbose):
6060
for key, value in config:
6161
ctx.obj.set_config(key, value)
6262

63+
# Dispose the DB engine deterministically when the command finishes, rather
64+
# than leaving it to garbage collection. On Python 3.13+ a lingering, GC'd
65+
# SQLite connection emits "ResourceWarning: unclosed database" at an
66+
# unpredictable time, which can leak into command output captured by tests.
67+
ctx.call_on_close(ctx.obj.close)
68+
6369
# Bo commands given
6470
if ctx.invoked_subcommand is None:
6571
# There is a connection but no subcommand

python/rcdb/provider.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,19 @@ def connect(self, connection_string="mysql+pymysql://rcdb@127.0.0.1/rcdb", check
150150
# Closes connection to data
151151
# ------------------------------------------------
152152
def disconnect(self):
153-
"""Closes connection to database"""
153+
"""Closes connection to database.
154+
155+
Closes the ORM session and disposes the SQLAlchemy engine so the
156+
underlying connection pool releases its DBAPI connections. Without the
157+
``engine.dispose()`` the pooled SQLite/MySQL connection stays open until
158+
the engine is garbage collected, which on Python 3.13+ surfaces as a
159+
``ResourceWarning: unclosed database`` at an unpredictable time.
160+
"""
154161
self._is_connected = False
155-
self.session.close()
162+
if self.session is not None:
163+
self.session.close()
164+
if self.engine is not None:
165+
self.engine.dispose()
156166

157167
# -------------------------------------------------
158168
# indicates ether the connection is open or not

0 commit comments

Comments
 (0)