Skip to content

test: add regression test for Actor.exit() deadlock from event listeners - #1061

Open
vdusek wants to merge 5 commits into
masterfrom
worktree-fix-b4
Open

test: add regression test for Actor.exit() deadlock from event listeners#1061
vdusek wants to merge 5 commits into
masterfrom
worktree-fix-b4

Conversation

@vdusek

@vdusek vdusek commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Actor.exit() / Actor.fail() called from within an event listener used to deadlock into a RecursionError, since the cleanup path waited on the very listener task that called it.

Fixed upstream in crawlee's EventManager: apify/crawlee-python#2088 (merged). On Python 3.11 it still deadlocks, because asyncio.wait_for there wraps the awaited coroutine in a separate task, which defeats crawlee's self-wait detection. This is a minor edge case, so we're not adding an SDK-side workaround just for Python 3.11.

This PR:

  • Adds a regression test for Actor.exit() called from an ABORTING listener, skipped on Python 3.11.
  • Bumps uv.lock (not the declared pyproject.toml constraint) to crawlee==1.9.1b4, so CI exercises the fix.

Follow-up: bump the declared crawlee constraint in pyproject.toml once a stable release with the fix ships.

✍️ Drafted by Claude Code

@vdusek vdusek added adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team. labels Jul 21, 2026
@vdusek vdusek self-assigned this Jul 21, 2026
@github-actions github-actions Bot added this to the 145th sprint - Tooling team milestone Jul 21, 2026
@github-actions github-actions Bot added the tested Temporary label used only programatically for some analytics. label Jul 21, 2026
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.17%. Comparing base (c471dde) to head (7e6ced2).

Additional details and impacted files
@@             Coverage Diff             @@
##           master    #1061       +/-   ##
===========================================
+ Coverage   56.63%   92.17%   +35.54%     
===========================================
  Files          51       51               
  Lines        3247     3247               
===========================================
+ Hits         1839     2993     +1154     
+ Misses       1408      254     -1154     
Flag Coverage Δ
e2e 35.20% <ø> (?)
integration 56.63% <ø> (ø)
unit 84.44% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vdusek
vdusek requested a review from Pijukatel July 21, 2026 11:06
@vdusek
vdusek marked this pull request as ready for review July 21, 2026 11:06
Comment thread src/apify/_actor.py Outdated
@vdusek

vdusek commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

maybe we can close this in favor of apify/crawlee-python#2088 , let's see

vdusek added a commit to apify/crawlee-python that referenced this pull request Aug 5, 2026
…rom within a listener (#2088)

Calling `EventManager.wait_for_all_listeners_to_complete()` - or closing
the manager via `__aexit__` - from *within* an event listener
deadlocked. The listener runs in a task that is itself registered in
`_listener_tasks`, so the wait ends up awaiting the very task that is
awaiting it. Under a close timeout this cycle degrades further into a
`RecursionError`.

Changes in `EventManager`:

- Listener tasks currently blocked in
`wait_for_all_listeners_to_complete()` are tracked in
`_waiting_listener_tasks` and excluded from the wait, so listener
waiters never await themselves or each other. A caller that is not a
listener is outside the cycle and still awaits every listener, waiting
ones included.
- The wait no longer wraps the gather in an inner task. The one-tick
defer that task provided is now explicit: `emit` only schedules the
listener wrappers, and each registers its listener task once it starts
running, so the wait yields before snapshotting `_listener_tasks`. This
also drops the `Event listener raised an exception.` log line, which
duplicated the ERROR the listener wrapper already logs with the
traceback.
- The listener wrapper's `finally` uses `set.discard()` instead of
`set.remove()`, since `__aexit__` may have already cleared the task set
while the listener was mid-flight (avoids a spurious `KeyError`).

Regression tests cover waiting from within a listener, several listeners
waiting at once, closing the manager from within a listener, and waiting
from outside while a listener is itself waiting.

This unblocks apify/apify-sdk-python#1061, where `Actor.exit()` is
called from inside an event listener (e.g. an `ABORTING` handler) - with
this fix the SDK can drop its `_detach_current_listener_task`
workaround.

*✍️ Drafted by Claude Code*
vdusek added 2 commits August 5, 2026 12:45
…ee lock

crawlee's EventManager now handles this deadlock upstream (apify/crawlee-python#2088),
so the SDK-side workaround is redundant. The lockfile is bumped to crawlee 1.9.1b4,
which contains the fix; the regression test passes without any SDK-side code changes.
The declared crawlee constraint in pyproject.toml stays >=1.8.0,<2.0.0 until crawlee
ships a stable release with the fix.
@vdusek vdusek changed the title fix: prevent deadlock when Actor.exit() is called from an event listener test: add regression test for Actor.exit() deadlock now fixed in crawlee Aug 5, 2026
@vdusek vdusek changed the title test: add regression test for Actor.exit() deadlock now fixed in crawlee fix: prevent deadlock when Actor.exit() is called from an event listener Aug 5, 2026
@vdusek vdusek changed the title fix: prevent deadlock when Actor.exit() is called from an event listener test: add regression test for Actor.exit() deadlock from event listeners, skipped on Python 3.11 Aug 5, 2026
@vdusek vdusek changed the title test: add regression test for Actor.exit() deadlock from event listeners, skipped on Python 3.11 test: add regression test for Actor.exit() deadlock from event listeners Aug 5, 2026
@vdusek
vdusek marked this pull request as draft August 5, 2026 11:58
@vdusek

vdusek commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Merge it with Crawlee 1.9.1 in the lockfile

@vdusek
vdusek requested a review from Mantisus August 5, 2026 12:27
@vdusek
vdusek marked this pull request as ready for review August 6, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants