Skip to content

Commit 76d5326

Browse files
febus982claude
andauthored
Fix atexit handler for async drivers during shutdown (#86)
* Fix atexit handler for async drivers during shutdown Use close=False when disposing engines in the atexit handler to avoid MissingGreenlet errors with async drivers (e.g., aiosqlite) that require an event loop context to close connections. The OS will clean up connections when the process exits. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Rename _dispose_sync to dispose_engines and make it public Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Document dispose_engines method and engine lifecycle Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 33c31ac commit 76d5326

File tree

3 files changed

+26
-6
lines changed

3 files changed

+26
-6
lines changed

docs/manager/config.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,23 @@ async with sa_manager.get_session() as session:
8888

8989
Note that async implementation has several differences from the sync one, make sure
9090
to check [SQLAlchemy asyncio documentation](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html)
91+
92+
## Bind engines lifecycle
93+
94+
Engine disposal is handled automatically by `SQLAlchemyBindManager`. Engines are disposed when:
95+
96+
* The manager instance is garbage collected
97+
* The Python interpreter shuts down (via an `atexit` handler)
98+
99+
In some scenarios, such as automated tests, you may need to manually dispose engines to release database connections between tests. The `dispose_engines()` method is available for this purpose:
100+
101+
```python
102+
sa_manager = SQLAlchemyBindManager(config)
103+
104+
# ... use the manager ...
105+
106+
# Manually dispose all engines
107+
sa_manager.dispose_engines()
108+
```
109+
110+
This method disposes all engines synchronously, including async engines (using their underlying sync engine).

sqlalchemy_bind_manager/_bind_manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def __init__(
9191
self.__init_bind(DEFAULT_BIND_NAME, config)
9292
SQLAlchemyBindManager._instances.add(self)
9393

94-
def _dispose_sync(self) -> None:
94+
def dispose_engines(self) -> None:
9595
"""Dispose all engines synchronously.
9696
9797
This method is safe to call from any context, including __del__
@@ -105,7 +105,7 @@ def _dispose_sync(self) -> None:
105105
bind.engine.dispose()
106106

107107
def __del__(self) -> None:
108-
self._dispose_sync()
108+
self.dispose_engines()
109109

110110
def __init_bind(self, name: str, config: SQLAlchemyConfig):
111111
if not isinstance(config, SQLAlchemyConfig):
@@ -232,4 +232,4 @@ def _cleanup_all_managers() -> None:
232232
called yet due to reference cycles or other GC timing issues.
233233
"""
234234
for manager in list(SQLAlchemyBindManager._instances):
235-
manager._dispose_sync()
235+
manager.dispose_engines()

tests/test_sqlalchemy_bind_manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ def test_atexit_cleanup_disposes_all_managers(multiple_config):
115115

116116
with patch.object(
117117
sa_manager,
118-
"_dispose_sync",
119-
) as mocked_dispose_sync:
118+
"dispose_engines",
119+
) as mocked_dispose_engines:
120120
_cleanup_all_managers()
121121

122-
mocked_dispose_sync.assert_called_once()
122+
mocked_dispose_engines.assert_called_once()

0 commit comments

Comments
 (0)