fix(memory): prevent close() race and enforce closed state in AsyncSQLiteSession - #3984
fix(memory): prevent close() race and enforce closed state in AsyncSQLiteSession#3984hsusul wants to merge 3 commits into
Conversation
seratch
left a comment
There was a problem hiding this comment.
Thanks for the contribution. The _closed state is appropriate here: checking _connection only inside the lock fixes concurrent close() calls, but it still allows an operation waiting behind close() to create a new connection immediately after shutdown completes.
Before merging, please add a controlled interleaving regression test where close() acquires the session lock first, an operation starts while close is paused, and close then completes. The test should verify that the waiting operation raises RuntimeError and that no connection is recreated. Please keep the _closed state and the existing post-close checks. This will lock down the terminal lifecycle behavior that the patch introduces.
Add a controlled regression for an operation waiting behind close() so it raises RuntimeError and does not recreate the connection.
|
Added test_async_sqlite_session_close_rejects_operation_waiting_on_lock to lock down the terminal lifecycle. It pauses close() while it holds the session lock (during connection close), starts get_items() so it waits on that lock, then lets close finish. The waiter raises RuntimeError("AsyncSQLiteSession is closed"), _connection stays None, and no new aiosqlite.connect occurs. Kept the _closed flag and existing post-close checks unchanged. |
seratch
left a comment
There was a problem hiding this comment.
Thanks for adding the controlled close-versus-operation interleaving test. The scenario and final assertions now cover the requested lifecycle behavior.
One test-lifecycle issue remains: assertions run while close() is intentionally paused, before release_close is set. If any of those assertions fail, the paused close task and the aiosqlite worker thread can remain alive, causing the regression test to hang instead of failing cleanly. Please guarantee release and task cleanup with try/finally, use a test-owned event instead of inspecting asyncio.Lock._waiters, and keep the synchronization waits bounded. The runtime implementation otherwise looks ready.
Guarantee release/task cleanup with try/finally, signal waiters via a test-owned lock event, and bound synchronization waits so assertion failures cannot hang the test.
|
Updated the interleaving test lifecycle: all mid-pause assertions are inside try/finally so release_close is always set and both tasks are cleaned up; waiting is signaled through a test-owned event on lock.acquire instead of asyncio.Lock._waiters; and the synchronization waits are bounded with asyncio.wait_for. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 77aa05344e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if self._closed: | ||
| raise RuntimeError("AsyncSQLiteSession is closed") |
There was a problem hiding this comment.
Reject empty writes after closure
When a caller invokes add_items([]) after close(), the method returns through its empty-list fast path before reaching this new _get_connection() guard, so a closed session silently accepts that public operation while reads and non-empty writes raise RuntimeError. Check the closed state before the empty-list return and cover this unsupported post-close case.
AGENTS.md reference: AGENTS.md:L62-L64
Useful? React with 👍 / 👎.
| # Multiple concurrent close calls should succeed without raising AttributeError | ||
| await asyncio.gather(session.close(), session.close(), session.close()) | ||
|
|
||
| # Additional sequential close call should also be a safe no-op |
There was a problem hiding this comment.
Terminate the new comments with periods
These two newly added comments are complete sentences but omit terminal periods, contrary to the repository's explicit comment-formatting requirement; append periods to both comments.
AGENTS.md reference: AGENTS.md:L201-L204
Useful? React with 👍 / 👎.
Summary
Fixes a concurrency race in
AsyncSQLiteSession.close()and enforces closed-state checks across session operations.AsyncSQLiteSession.close()evaluatedif self._connection is None:outsideself._lock. When concurrent tasks (e.g. during application shutdown or teardown) calledclose(), both passed theNonecheck. The first task acquiredself._lock, closed the connection, and setself._connection = None. The second task then acquiredself._lockand executedawait self._connection.close(), raisingAttributeError: 'NoneType' object has no attribute 'close'.AsyncSQLiteSessionlacked aself._closedstate flag (unlikeSQLiteSession). Consequently, invokingget_items(),add_items(),pop_item(), orclear_session()afterclose()re-initialized a database connection instead of raisingRuntimeError("AsyncSQLiteSession is closed").Solution:
self._closedflag toAsyncSQLiteSession.close()to acquireself._lock, checkself._closed, markself._closed = True, and safely closeself._connectionif active._get_connection()to checkself._closedboth outside and insideself._init_lock, raisingRuntimeError("AsyncSQLiteSession is closed").Test plan
test_async_sqlite_session_concurrent_closeintests/extensions/memory/test_async_sqlite_session.pyto verify that concurrent (asyncio.gather) and repeated calls toclose()succeed cleanly.test_async_sqlite_session_closed_operations_raise_runtime_errorto verify that callingget_items(),add_items(),pop_item(), orclear_session()on a closed session raisesRuntimeError("AsyncSQLiteSession is closed").make format(passed, 841 files unchanged)make lint(passed, 0 errors)uv run pyright src/agents/extensions/memory/async_sqlite_session.py tests/extensions/memory/test_async_sqlite_session.py(passed, 0 errors)uv run pytest tests/extensions/memory/test_async_sqlite_session.py -v(20 passed)git diff --check(passed, 0 warnings)Issue number
Closes #3983
Checks
.agents/skills/code-change-verification/scripts/run.sh/reviewbefore submitting this PR