Skip to content

Comments

Raise ReentrantRequestError on reentrant sync_req call#37

Merged
shuckc merged 1 commit intomasterfrom
raise-reentrant-sync-req
Feb 21, 2026
Merged

Raise ReentrantRequestError on reentrant sync_req call#37
shuckc merged 1 commit intomasterfrom
raise-reentrant-sync-req

Conversation

@shuckc
Copy link
Member

@shuckc shuckc commented Feb 20, 2026

When reader_to_context_task dispatches an async message (or sync request) 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 for this specific deadlock scenario.
  2. Track the reader task in _reader_task attribute of KdbWriter (line 73), set at the start of reader_to_context_task via asyncio.current_task().
  3. Detection in sync_req() 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

…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
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ReentrantRequestError exception to signal when sync_req() is called from within a reader task handler
  • Added reader task tracking via _reader_task attribute in KdbWriter to detect reentrant calls
  • Added detection logic in sync_req() to raise ReentrantRequestError before 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()
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

# send an async message to the server, triggering the handler
client_wr.write(cv("hello"), MessageType.ASYNC)
await asyncio.sleep(0.1)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
@shuckc shuckc merged commit a61b325 into master Feb 21, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant