Raise ReentrantRequestError on reentrant sync_req call#37
Conversation
…est) to a handler, and that handler calls sync_req() back on the same connection, it deadlocks. The reader task is the only thing that can read the response, but it's blocked waiting for the handler to return. Fix (3 changes in aiokdb/server.py): 1. New exception ReentrantRequestError at line 23 — clear error class for this specific deadlock scenario. 2. Track the reader task — added _reader_task attribute to KdbWriter (line 73), set at the start of reader_to_context_task via asyncio.current_task() (line 233). 3. Detection in sync_req() (lines 99-109) — before creating the Future, checks if the current task is the reader task. If so, raises ReentrantRequestError with a message suggesting asyncio.create_task() as the workaround. Tests added to test/test_client_server.py: - test_reentrant_sync_req_from_async_handler — verifies the error is raised when an on_async_message handler calls sync_req - test_reentrant_sync_req_from_sync_handler — verifies the error is raised when a server's on_sync_request handler calls sync_req back to the client, and that it propagates as a KException to the original caller
There was a problem hiding this comment.
Pull request overview
This PR prevents a deadlock scenario in the aiokdb library that occurs when a message or request handler calls sync_req() back on the same connection. The deadlock happens because the reader task is the only component that can read the response, but it's blocked waiting for the handler to return.
Changes:
- Introduced
ReentrantRequestErrorexception to signal whensync_req()is called from within a reader task handler - Added reader task tracking via
_reader_taskattribute inKdbWriterto detect reentrant calls - Added detection logic in
sync_req()to raiseReentrantRequestErrorbefore creating the request, preventing the deadlock
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| aiokdb/server.py | Added ReentrantRequestError exception class, _reader_task attribute to track the reader task, and detection logic in sync_req() to raise the exception when called reentrantly |
| test/test_client_server.py | Added two tests to verify ReentrantRequestError is raised: one for async message handlers and one for sync request handlers |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async def reader_to_context_task( | ||
| q_writer: KdbWriter, q_reader: KdbReader, context: BaseContext | ||
| ) -> None: | ||
| q_writer._reader_task = asyncio.current_task() |
There was a problem hiding this comment.
The _reader_task attribute is set here but never cleared when the reader_to_context_task function exits (in the finally block at line 288-292). While KdbWriter objects are typically not reused, it would be more defensive to clear this reference in the finally block to prevent potential issues if the object lifetime exceeds the task lifetime or if the object is reused in unexpected ways. Consider adding q_writer._reader_task = None in the finally block.
|
|
||
| # send an async message to the server, triggering the handler | ||
| client_wr.write(cv("hello"), MessageType.ASYNC) | ||
| await asyncio.sleep(0.1) |
There was a problem hiding this comment.
Using a fixed sleep of 0.1 seconds to wait for the async handler to execute is fragile and can lead to flaky tests. The test might pass on fast machines but fail on slower ones, or vice versa. Consider using a more robust synchronization mechanism, such as an asyncio.Event that the handler sets when it catches the exception, and await that event with a timeout. For example: event = asyncio.Event(), then in the handler after catching: event.set(), and before assertions: await asyncio.wait_for(event.wait(), timeout=1.0).
When
reader_to_context_taskdispatches an async message (or sync request) to a handler, and that handler callssync_req()back on the same connection, it deadlocks. The reader task is the only thing that can read the response, but it's blocked waiting for the handler to return.Fix (3 changes in aiokdb/server.py):
ReentrantRequestErrorfor this specific deadlock scenario._reader_taskattribute ofKdbWriter(line 73), set at the start ofreader_to_context_taskviaasyncio.current_task().sync_req()before creating theFuture, checks if the current task is the reader task. If so, raisesReentrantRequestErrorwith a message suggestingasyncio.create_task()as the workaround.Tests added to
test/test_client_server.py:test_reentrant_sync_req_from_async_handler— verifies the error is raised when anon_async_messagehandler callssync_req.test_reentrant_sync_req_from_sync_handler— verifies the error is raised when a server'son_sync_requesthandler callssync_reqback to the client, and that it propagates as aKExceptionto the original caller