Skip to content

Commit 4a0c09f

Browse files
committed
fix: exit already-entered contexts when Actor or event manager init fails
A failed `__aenter__` never triggers `__aexit__`, so when the charging manager or the platform events websocket failed to initialize, the already-entered event manager was left active with its recurring persist-state task dangling.
1 parent 99ea41f commit 4a0c09f

4 files changed

Lines changed: 27 additions & 1 deletion

File tree

src/apify/_actor.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,12 @@ async def __aenter__(self) -> Self:
197197
self.log.debug('Event manager initialized')
198198

199199
# Initialize the charging manager.
200-
await self._charging_manager_implementation.__aenter__()
200+
try:
201+
await self._charging_manager_implementation.__aenter__()
202+
except BaseException:
203+
# Exit the already-entered event manager so its recurring tasks do not leak.
204+
await self.event_manager.__aexit__(None, None, None)
205+
raise
201206
self.log.debug('Charging manager initialized')
202207

203208
# Mark initialization as complete and update global state.

src/apify/events/_apify_event_manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ async def __aenter__(self) -> Self:
7878
)
7979
is_connected = await self._connected_to_platform_websocket
8080
if not is_connected:
81+
# Exit the already-entered parent so the recurring persist state task does not leak.
82+
await self.__aexit__(None, None, None)
8183
raise RuntimeError('Error connecting to platform events websocket!')
8284
else:
8385
logger.debug('APIFY_ACTOR_EVENTS_WS_URL env var not set, no events from Apify platform will be emitted.')

tests/unit/actor/test_actor_lifecycle.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from ..._utils import poll_until_condition
2020
from apify import Actor
21+
from apify._charging import ChargingManagerImplementation
2122
from apify._consts import EXIT_CODE_ERROR_USER_FUNCTION_THREW, ActorEnvVars, ApifyEnvVars
2223

2324
if TYPE_CHECKING:
@@ -112,6 +113,19 @@ async def test_fail_properly_deinitializes_actor(actor: _ActorType) -> None:
112113
assert actor._active is False
113114

114115

116+
async def test_failed_charging_manager_init_does_not_leak_event_manager() -> None:
117+
"""Test that a failure in the charging manager's `__aenter__` also exits the already-entered event manager."""
118+
actor = Actor()
119+
with (
120+
mock.patch.object(ChargingManagerImplementation, '__aenter__', side_effect=RuntimeError('Charging failed')),
121+
pytest.raises(RuntimeError, match='Charging failed'),
122+
):
123+
await actor.init()
124+
125+
assert actor._active is False
126+
assert actor.event_manager.active is False
127+
128+
115129
async def test_actor_handles_exceptions_and_cleans_up_properly() -> None:
116130
"""Test that Actor properly cleans up when an exception occurs in the async context manager."""
117131
actor = None

tests/unit/events/test_apify_event_manager.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,18 @@ async def event_handler(data: Any) -> None:
162162

163163

164164
async def test_lifecycle_on_platform_without_websocket(monkeypatch: pytest.MonkeyPatch) -> None:
165+
"""Test that a failed websocket connection raises and also exits the parent's recurring persist state task."""
165166
monkeypatch.setenv(ActorEnvVars.EVENTS_WEBSOCKET_URL, 'ws://localhost:56565')
166167
event_manager = ApifyEventManager(Configuration.get_global_configuration())
167168

168169
with pytest.raises(RuntimeError, match=r'Error connecting to platform events websocket!'):
169170
async with event_manager:
170171
pass
171172

173+
assert event_manager.active is False
174+
persist_state_task = event_manager._emit_persist_state_event_rec_task.task
175+
assert persist_state_task is None or persist_state_task.done()
176+
172177

173178
async def test_lifecycle_on_platform(monkeypatch: pytest.MonkeyPatch) -> None:
174179
async with (

0 commit comments

Comments
 (0)