|
4 | 4 | import contextlib |
5 | 5 | import json |
6 | 6 | import logging |
7 | | -from datetime import UTC, datetime |
| 7 | +from datetime import UTC, datetime, timedelta |
8 | 8 | from typing import TYPE_CHECKING, Any |
9 | 9 | from unittest import mock |
10 | 10 | from unittest.mock import AsyncMock, Mock |
|
14 | 14 | import websockets.asyncio.server |
15 | 15 |
|
16 | 16 | from apify_client._models import Run |
17 | | -from crawlee.events._types import Event, EventPersistStateData |
| 17 | +from crawlee.events._types import Event, EventAbortingData, EventPersistStateData |
18 | 18 |
|
19 | 19 | from ..._utils import poll_until_condition |
20 | 20 | from apify import Actor |
@@ -112,6 +112,29 @@ async def test_fail_properly_deinitializes_actor(actor: _ActorType) -> None: |
112 | 112 | assert actor._active is False |
113 | 113 |
|
114 | 114 |
|
| 115 | +async def test_exit_from_event_listener_completes_cleanup(caplog: pytest.LogCaptureFixture) -> None: |
| 116 | + """`Actor.exit()` called from an event listener runs cleanup instead of deadlocking into a RecursionError.""" |
| 117 | + actor = Actor(exit_process=False) |
| 118 | + await actor.init() |
| 119 | + |
| 120 | + exit_returned = False |
| 121 | + |
| 122 | + async def on_aborting(_data: EventAbortingData) -> None: |
| 123 | + nonlocal exit_returned |
| 124 | + await actor.exit(event_listeners_timeout=timedelta(seconds=1)) |
| 125 | + exit_returned = True |
| 126 | + |
| 127 | + actor.on(Event.ABORTING, on_aborting) |
| 128 | + actor.event_manager.emit(event=Event.ABORTING, event_data=EventAbortingData()) |
| 129 | + |
| 130 | + await poll_until_condition(lambda: not actor._active, timeout=5, poll_interval=0.1) |
| 131 | + |
| 132 | + assert exit_returned, 'Actor.exit() never returned inside the listener (deadlocked).' |
| 133 | + assert actor._active is False |
| 134 | + assert actor.event_manager.active is False |
| 135 | + assert 'RecursionError' not in caplog.text |
| 136 | + |
| 137 | + |
115 | 138 | async def test_failed_charging_manager_init_does_not_leak_event_manager() -> None: |
116 | 139 | """Test that a failure in the charging manager's `__aenter__` also exits the already-entered event manager.""" |
117 | 140 | actor = Actor() |
|
0 commit comments