From af798d67718286d63fdc5723494c7b6aa1b5226a Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 4 Aug 2026 14:32:51 +0200 Subject: [PATCH 1/2] fix: do not abort Actor exit when the terminal status message fails --- src/apify/_actor.py | 5 ++++- tests/unit/actor/test_actor_lifecycle.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 077759f4..9cf699e1 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -260,7 +260,10 @@ async def __aexit__( async def finalize() -> None: if self.status_message is not None: - await self.set_status_message(self.status_message, is_terminal=True) + try: + await self.set_status_message(self.status_message, is_terminal=True) + except Exception: + self.log.exception('Failed to set terminal status message') # Sleep for a bit so that the listeners have a chance to trigger await asyncio.sleep(0.1) diff --git a/tests/unit/actor/test_actor_lifecycle.py b/tests/unit/actor/test_actor_lifecycle.py index a702cd09..35ca13e4 100644 --- a/tests/unit/actor/test_actor_lifecycle.py +++ b/tests/unit/actor/test_actor_lifecycle.py @@ -410,6 +410,26 @@ async def test_actor_fail_prevents_further_execution(caplog: pytest.LogCaptureFi assert status_records[0].levelno == logging.INFO +async def test_failing_terminal_status_message_does_not_abort_exit(monkeypatch: pytest.MonkeyPatch) -> None: + """A failing terminal status message must not skip the remaining cleanup steps nor the final exit code.""" + charging_manager_exit = AsyncMock() + save_actor_state = AsyncMock() + monkeypatch.setattr(_ActorType, 'set_status_message', AsyncMock(side_effect=RuntimeError('Status update failed'))) + monkeypatch.setattr(ChargingManagerImplementation, '__aexit__', charging_manager_exit) + monkeypatch.setattr(_ActorType, '_save_actor_state', save_actor_state) + + actor = Actor(exit_process=True) + await actor.init() + + with pytest.raises(SystemExit) as exc_info: + await actor.exit(exit_code=7, status_message='Done') + + assert exc_info.value.code == 7 + assert actor.event_manager.active is False + charging_manager_exit.assert_called_once() + save_actor_state.assert_called_once() + + @pytest.mark.parametrize( ('first_with_call', 'second_with_call'), [ From ab492b4f2657d64b310230838e4fc0513acc46de Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 4 Aug 2026 14:47:20 +0200 Subject: [PATCH 2/2] test: assert the terminal status message failure is attempted and logged --- tests/unit/actor/test_actor_lifecycle.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/unit/actor/test_actor_lifecycle.py b/tests/unit/actor/test_actor_lifecycle.py index 35ca13e4..9f73df02 100644 --- a/tests/unit/actor/test_actor_lifecycle.py +++ b/tests/unit/actor/test_actor_lifecycle.py @@ -410,20 +410,28 @@ async def test_actor_fail_prevents_further_execution(caplog: pytest.LogCaptureFi assert status_records[0].levelno == logging.INFO -async def test_failing_terminal_status_message_does_not_abort_exit(monkeypatch: pytest.MonkeyPatch) -> None: - """A failing terminal status message must not skip the remaining cleanup steps nor the final exit code.""" +async def test_failing_terminal_status_message_does_not_abort_exit( + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A failing terminal status message must be logged and must not skip the rest of the cleanup nor the exit code.""" + set_status_message = AsyncMock(side_effect=RuntimeError('Status update failed')) charging_manager_exit = AsyncMock() save_actor_state = AsyncMock() - monkeypatch.setattr(_ActorType, 'set_status_message', AsyncMock(side_effect=RuntimeError('Status update failed'))) + monkeypatch.setattr(_ActorType, 'set_status_message', set_status_message) monkeypatch.setattr(ChargingManagerImplementation, '__aexit__', charging_manager_exit) monkeypatch.setattr(_ActorType, '_save_actor_state', save_actor_state) + # Explicitly set exit_process=True since in Pytest env it defaults to False. actor = Actor(exit_process=True) await actor.init() with pytest.raises(SystemExit) as exc_info: await actor.exit(exit_code=7, status_message='Done') + # The mock replaces a class attribute, so it is not bound and does not receive `self`. + set_status_message.assert_called_once_with('Done', is_terminal=True) + assert [r for r in caplog.records if r.msg == 'Failed to set terminal status message'] assert exc_info.value.code == 7 assert actor.event_manager.active is False charging_manager_exit.assert_called_once()