-
Notifications
You must be signed in to change notification settings - Fork 2
[Bugfix] Fix DPM replies timeout task context #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
beauremus
wants to merge
2
commits into
chore/configure-pytest
Choose a base branch
from
fix/dpm-replies-task-context
base: chore/configure-pytest
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
61 changes: 61 additions & 0 deletions
61
.github/WORKFLOW_TASKS/issue-2-bugfix-dpm-replies-timeout-task-context.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # Issue 2: [Bugfix] Resolve `RuntimeError('Timeout should be used inside a task')` in `acsys.dpm.replies()` on Python 3.11+ | ||
|
|
||
| **Status:** Implemented on branch `fix/dpm-replies-task-context`; full pytest suite passes locally. Draft PR pending. | ||
|
|
||
| ## Summary | ||
|
|
||
| Make `acsys.dpm.DPM.replies()` usable when its async generator is driven through an event loop on Python 3.11 and later, without exposing the `asyncio.wait_for()` task-context runtime error. | ||
|
|
||
| ## Reproduction and current evidence | ||
|
|
||
| `tests/test_dpm_task_context.py` reproduces the problem in the RED phase by: | ||
|
|
||
| 1. Creating a bare `DPM` instance with a stubbed `__anext__`. | ||
| 2. Creating `replies_gen = dpm.replies(tmo=1.0)`. | ||
| 3. Driving the async generator once with `.send(None)` while marking a bare event loop as running, without wrapping the generator in an active task. | ||
| 4. Asserting that Python 3.11+ raises `RuntimeError` matching `Timeout should be used inside a task`. | ||
|
|
||
| The GREEN regression test continues the manually driven generator after the fix and verifies that the first yielded object is a task and that the reply is returned successfully. | ||
|
|
||
| The relevant implementation is `DPM.replies()` in `acsys/dpm/__init__.py`, where each reply is awaited through `asyncio.wait_for(self.__anext__(), tmo)`. | ||
|
|
||
| ## Scope | ||
|
|
||
| - Diagnose the supported invocation patterns for `DPM.replies()` and the Python 3.11+ `asyncio` requirement involved in the failure. | ||
| - Change the implementation and/or surrounding async boundary so the public replies API no longer fails solely because the caller drives the async generator without an existing task. | ||
| - Preserve timeout semantics: `tmo` remains the maximum interval between replies and an `asyncio.TimeoutError` remains the expected timeout signal. | ||
| - Replace the current regression test's failure expectation with a success/behavioral regression test that covers the supported calling pattern. | ||
|
|
||
| ## Acceptance criteria | ||
|
|
||
| - The reproducer no longer raises `RuntimeError('Timeout should be used inside a task')` on Python 3.11+. | ||
| - Iterating `DPM.replies(tmo=...)` from a normal task continues to yield replies as before. | ||
| - A timeout between replies still raises `asyncio.TimeoutError` with the documented behavior. | ||
| - Cancellation and generator cleanup do not leave pending tasks or unhandled coroutine warnings. | ||
| - The regression coverage runs under the repository's supported Python versions, or any version-specific limitation is explicitly documented. | ||
| - No unrelated changes to DPM request/reconnection behavior are introduced. | ||
|
|
||
| ## Verification | ||
|
|
||
| - RED phase: the `.send(None)` reproducer failed before the implementation change with `RuntimeError('Timeout should be used inside a task')`. | ||
| - GREEN phase: `python -m pytest -q tests/test_dpm_task_context.py -W error::RuntimeWarning` passed. | ||
| - Full suite: `python -m pytest -q` passed (`1 passed`). | ||
| - Focused regression test passed with runtime warnings treated as errors. | ||
| - The fix creates a task for `self.__anext__()` and a task for the `asyncio.wait_for()` coroutine, keeping Python 3.11+ timeout context execution inside a task. | ||
|
|
||
| ## Dependencies and risks | ||
|
|
||
| - Issue 1 should establish the pytest/pytest-asyncio configuration before this issue is implemented. | ||
| - The exact repair must preserve the caller's event-loop ownership and avoid silently creating a competing loop. | ||
| - The test's current bare-object setup bypasses normal `DPM` initialization; implementation should also be validated through the real public setup path where practical. | ||
|
|
||
| ## Review focus | ||
|
|
||
| - Whether the fix addresses the task-context boundary rather than masking the exception. | ||
| - Whether timeout, cancellation, and async-generator lifecycle semantics remain correct. | ||
| - Whether tests assert observable API behavior instead of depending only on a particular `asyncio` implementation detail. | ||
|
|
||
| ## Out of scope | ||
|
|
||
| - Replacing the synchronization primitives used by `set_many()` (Issue 3). | ||
| - General modernization of all `asyncio.get_event_loop()` calls in the package. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -140,6 +140,5 @@ cython_debug/ | |
|
|
||
| # Application specific | ||
| acsys.tgz | ||
| tests/ | ||
| .python-version | ||
| .vscode | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import asyncio | ||
| import pytest | ||
| import acsys.dpm | ||
|
|
||
|
|
||
| def _new_loop(): | ||
| """Create a test loop without leaking the loop created during import.""" | ||
| try: | ||
| current_loop = asyncio.get_event_loop() | ||
| except RuntimeError: | ||
| current_loop = None | ||
| if current_loop is not None and not current_loop.is_running(): | ||
| current_loop.close() | ||
|
|
||
| loop = asyncio.new_event_loop() | ||
| asyncio.set_event_loop(loop) | ||
| return loop | ||
|
|
||
|
|
||
| def _drive_once_without_task(coro, loop): | ||
| """Advance a coroutine once without wrapping it in an asyncio.Task.""" | ||
| asyncio.events._set_running_loop(loop) | ||
| try: | ||
| return coro.send(None) | ||
| finally: | ||
| asyncio.events._set_running_loop(None) | ||
|
|
||
|
|
||
| def _close_loop(loop): | ||
| pending = asyncio.all_tasks(loop) | ||
| for task in pending: | ||
| task.cancel() | ||
| if pending: | ||
| loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) | ||
| asyncio.set_event_loop(None) | ||
| loop.close() | ||
|
|
||
|
|
||
| def test_dpm_replies_timeout_uses_task_context(): | ||
| """DPM.replies() can start without an existing asyncio.Task.""" | ||
| dpm = object.__new__(acsys.dpm.DPM) | ||
|
|
||
| async def dummy_anext(): | ||
| await asyncio.sleep(0.01) | ||
| return "data" | ||
|
|
||
| dpm.__anext__ = dummy_anext | ||
| loop = _new_loop() | ||
| coro = dpm.replies(tmo=1.0).__anext__() | ||
|
|
||
| try: | ||
| timeout_task = _drive_once_without_task(coro, loop) | ||
| assert isinstance(timeout_task, asyncio.Task) | ||
| assert loop.run_until_complete(timeout_task) == "data" | ||
| with pytest.raises(StopIteration) as stopped: | ||
| coro.send(None) | ||
| assert stopped.value.value == "data" | ||
| finally: | ||
| coro.close() | ||
| _close_loop(loop) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two layers of tasks still seems a bit much. Can it be written as: