Skip to content

Commit 4feaf6d

Browse files
Fix issue with actor restarting instead of stopping
When an actor threw an exception during cancellation, it restarted rather than stopping as expected. Now it stops and raises an unhandled exception. Signed-off-by: Elzbieta Kotulska <[email protected]>
1 parent 6b40e77 commit 4feaf6d

File tree

3 files changed

+49
-2
lines changed

3 files changed

+49
-2
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@
1414

1515
## Bug Fixes
1616

17-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
17+
- Fixed issue where actors would restart instead of stopping when exceptions occurred during cancellation. Actors now properly stop and surface the unhandled exception.

src/frequenz/sdk/actor/_actor.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,31 @@ class Actor(BackgroundService, abc.ABC):
4444
This is mostly used for testing purposes and shouldn't be set in production.
4545
"""
4646

47+
def __init__(self, *, name: str | None = None) -> None:
48+
"""Create actor instance.
49+
50+
Args:
51+
name: The name of this background service.
52+
"""
53+
super().__init__(name=name)
54+
self._is_cancelled = False
55+
4756
def start(self) -> None:
4857
"""Start this actor.
4958
5059
If this actor is already running, this method does nothing.
5160
"""
5261
if self.is_running:
5362
return
63+
64+
if self._is_cancelled:
65+
_logger.error(
66+
(
67+
"Actor %s: was canceled and can't be re-run."
68+
"Please create another actor instance.",
69+
),
70+
self,
71+
)
5472
self._tasks.clear()
5573
self._tasks.add(asyncio.create_task(self._run_loop()))
5674

@@ -94,6 +112,12 @@ async def _run_loop(self) -> None:
94112
_logger.info("Actor %s: Cancelled.", self)
95113
raise
96114
except Exception: # pylint: disable=broad-except
115+
if self._is_cancelled:
116+
_logger.exception(
117+
"Actor %s: Raised an unhandled exception during stop.", self
118+
)
119+
raise
120+
97121
_logger.exception("Actor %s: Raised an unhandled exception.", self)
98122
limit_str = "∞" if self._restart_limit is None else self._restart_limit
99123
limit_str = f"({n_restarts}/{limit_str})"
@@ -113,3 +137,14 @@ async def _run_loop(self) -> None:
113137
break
114138

115139
_logger.info("Actor %s: Stopped.", self)
140+
141+
def cancel(self, msg: str | None = None) -> None:
142+
"""Cancel actor.
143+
144+
Cancelled actor can't be started again.
145+
146+
Args:
147+
msg: The message to be passed to the tasks being cancelled.
148+
"""
149+
self._is_cancelled = True
150+
return super().cancel(msg)

tests/actor/test_actor.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,5 +389,17 @@ async def test_actor_stop_if_error_was_raised_during_cancel(
389389
assert msg == 5
390390
assert actor.is_running is True
391391

392-
await actor.stop()
392+
# Actor raises unhandled exception
393+
# this exception is propagated to user
394+
# during stop.
395+
with pytest.raises(ExceptionGroup):
396+
await actor.stop()
393397
assert actor.restart_count == 0
398+
399+
assert caplog.record_tuples == [
400+
(*ACTOR_INFO, "Actor RaiseExceptionOnCancelActor[test]: Started."),
401+
(
402+
*ACTOR_ERROR,
403+
"Actor RaiseExceptionOnCancelActor[test]: Raised an unhandled exception during stop.",
404+
),
405+
]

0 commit comments

Comments
 (0)