🤞🤞Double Cross

The File That Never Says Goodbye

TL;DR: AsyncConnection has close() method but no __aenter__() - it's a context manager that only knows goodbye

#python#resource-management#file-handling#context-manager

The Code

python
1from typing import Any
2
3from .base import ProxyComparable, StartableContext
4
5def create_async_engine(*arg, **kw) -> AsyncEngine: ...
6
7class AsyncConnectable: ...
8
9class AsyncConnection(ProxyComparable, StartableContext, AsyncConnectable):
10    engine: Any
11    sync_engine: Any
12    sync_connection: Any
13    def __init__(self, async_engine, sync_connection: Any | None = ...) -> None: ...
14    async def start(self, is_ctxmanager: bool = ...): ...
15    @property
16    def connection(self) -> None: ...
17    async def get_raw_connection(self): ...
18    @property
19    def info(self): ...
20    def begin(self): ...
21    def begin_nested(self): ...
22    async def invalidate(self, exception: Any | None = ...): ...
23    async def get_isolation_level(self): ...
24    async def set_isolation_level(self): ...
25    def in_transaction(self): ...
26    def in_nested_transaction(self): ...
27    def get_transaction(self): ...
28    def get_nested_transaction(self): ...
29    async def execution_options(self, **opt): ...
30    async def commit(self) -> None: ...
31    async def rollback(self) -> None: ...
32    async def close(self) -> None: ...
33    async def exec_driver_sql(self, statement, parameters: Any | None = ..., execution_options=...): ...
34    async def stream(self, statement, parameters: Any | None = ..., execution_options=...): ...
35    async def execute(self, statement, parameters: Any | None = ..., execution_options=...): ...
36    async def scalar(self, statement, parameters: Any | None = ..., execution_options=...): ...
37    async def scalars(self, statement, parameters: Any | None = ..., execution_options=...): ...
38    async def stream_scalars(self, statement, parameters: Any | None = ..., execution_options=...): ...
39    async def run_sync(self, fn, *arg, **kw): ...
40    def __await__(self): ...
41    async def __aexit__(self, type_, value, traceback) -> None: ...
42    # proxied from Connection
43    dialect: Any
44    @property
45    def closed(self): ...
46    @property
47    def invalidated(self): ...
48    @property
49    def default_isolation_level(self): ...
50
51class AsyncEngine(ProxyComparable, AsyncConnectable):
52    class _trans_ctx(StartableContext):
53        conn: Any
54        def __init__(self, conn) -> None: ...
55        transaction: Any
56        async def start(self, is_ctxmanager: bool = ...): ...
57        async def __aexit__(self, type_, value, traceback) -> None: ...
58    sync_engine: Any
59    def __init__(self, sync_engine) -> None: ...
60    def begin(self): ...
61    def connect(self): ...
62    async def raw_connection(self): ...
63    def execution_options(self, **opt): ...
64    async def dispose(self): ...
65    # proxied from Engine
66    url: Any
67    pool: Any
68    dialect: Any
69    echo: Any
70    @property
71    def engine(self): ...
72    @property
73    def name(self): ...
74    @property
75    def driver(self): ...
76    def clear_compiled_cache(self) -> None: ...
77    def update_execution_options(self, **opt) -> None: ...
78    def get_execution_options(self): ...
79
80class AsyncTransaction(ProxyComparable, StartableContext):
81    connection: Any
82    sync_transaction: Any
83    nested: Any
84    def __init__(self, connection, nested: bool = ...) -> None: ...
85    @property
86    def is_valid(self): ...
87    @property
88    def is_active(self): ...
89    async def close(self) -> None: ...
90    async def rollback(self) -> None: ...
91    async def commit(self) -> None: ...
92    async def start(self, is_ctxmanager: bool = ...): ...
93    async def __aexit__(self, type_, value, traceback) -> None: ...
94

The Prayer 🤞🤞

🤞🤞 Maybe if I just call close() manually everywhere, I won't leak database connections like a sieve. Surely I'll remember to close every single connection in every code path, including the ones where exceptions happen. What could possibly go wrong with manual resource management in async code?

The Reality Check

This AsyncConnection class implements aexit() for context manager cleanup but appears to be missing aenter(). This creates a half-baked context manager that will crash when you try to use it with 'async with'. Even worse, without proper context manager support, developers are forced to manually manage connection lifecycles, which is exactly the kind of error-prone drudgery that context managers were invented to eliminate.

In production, this leads to connection leaks faster than you can say "connection pool exhausted." Every forgotten close() call is a database connection that lives forever, slowly strangling your application. Your monitoring will show mysterious connection pool timeouts, and your database will eventually refuse new connections entirely. The async nature makes it even trickier to track down which code paths aren't properly cleaning up, especially when exceptions interrupt the normal flow.

The Fix

The fix is straightforward: implement aenter() to complete the context manager protocol. The connection needs to return itself when entering the context:

python
1class AsyncConnection(ProxyComparable, StartableContext, AsyncConnectable):
2    # ... existing code ...
3    
4    async def __aenter__(self) -> 'AsyncConnection':
5        await self.start(is_ctxmanager=True)
6        return self
7    
8    async def __aexit__(self, type_, value, traceback) -> None:
9        await self.close()
10

With this fix, you can use the connection safely with async context managers:

python
1# Now this works without exploding
2async with AsyncConnection(engine) as conn:
3    result = await conn.execute("SELECT * FROM users")
4    # Connection automatically closed, even if exceptions occur
5

For extra safety, consider adding a warning or error if someone tries to use the connection without the context manager, helping catch manual resource management attempts before they become production leaks.

Lesson Learned

Context managers are only as good as their enter and exit methods - implement both or implement neither.