Skip to content

Commit bcfbd5b

Browse files
Stop raising CancelledError when actor is cancelled (frequenz-floss#1166)
Cancelling BackgroundService should not raise exception. But we see exception when we stop actor, after calling `actor.wait()`.
2 parents 855a181 + eff167b commit bcfbd5b

File tree

3 files changed

+15
-68
lines changed

3 files changed

+15
-68
lines changed

RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@
1717
## Bug Fixes
1818

1919
- Fixed bug with formulas raising exception when stopped.
20+
21+
- Fix a bug that raised `CancelledError` when actor was started with `frequenz.sdk.actor.run` and stopped.
22+
23+
- Stop catching `BaseException` in `frequenz.sdk.actor.run`. Only `CancelledError` and `Exception` are caught now.

src/frequenz/sdk/actor/_run_utils.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,21 @@ async def run(*actors: Actor) -> None:
3838
done_tasks, pending_tasks = await asyncio.wait(
3939
pending_tasks, return_when=asyncio.FIRST_COMPLETED
4040
)
41-
4241
# This should always be only one task, but we handle many for extra safety
4342
for task in done_tasks:
44-
# Cancellation needs to be checked first, otherwise the other methods
45-
# could raise a CancelledError
46-
if task.cancelled():
43+
# BackgroundService returns a BaseExceptionGroup containing multiple
44+
# exceptions. The 'task.result()' statement raises these exceptions,
45+
# and 'except*' is used to handle them as a group. If the task raises
46+
# multiple different exceptions, 'except*' will be invoked multiple times,
47+
# once for each exception group.
48+
try:
49+
task.result()
50+
except* asyncio.CancelledError:
4751
_logger.info("Actor %s: Cancelled while running.", task.get_name())
48-
elif exception := task.exception():
49-
_logger.error(
52+
except* Exception: # pylint: disable=broad-exception-caught
53+
_logger.exception(
5054
"Actor %s: Raised an exception while running.",
5155
task.get_name(),
52-
exc_info=exception,
5356
)
5457
else:
5558
_logger.info("Actor %s: Finished normally.", task.get_name())

tests/actor/test_actor.py

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -78,31 +78,6 @@ async def _run(self) -> None:
7878
print(f"{self} done (should not happen)")
7979

8080

81-
class RaiseBaseExceptionActor(BaseTestActor):
82-
"""A faulty actor that raises a BaseException as soon as it receives a message."""
83-
84-
def __init__(
85-
self,
86-
recv: Receiver[int],
87-
) -> None:
88-
"""Create an instance.
89-
90-
Args:
91-
recv: A channel receiver for int data.
92-
"""
93-
super().__init__(name="test")
94-
self._recv = recv
95-
96-
async def _run(self) -> None:
97-
"""Start the actor and crash upon receiving a message."""
98-
print(f"{self} started")
99-
self.inc_restart_count()
100-
async for _ in self._recv:
101-
print(f"{self} is about to crash")
102-
raise MyBaseException("This is a test")
103-
print(f"{self} done (should not happen)")
104-
105-
10681
ACTOR_INFO = ("frequenz.sdk.actor._actor", 20)
10782
ACTOR_ERROR = ("frequenz.sdk.actor._actor", 40)
10883
RUN_INFO = ("frequenz.sdk.actor._run_utils", 20)
@@ -309,41 +284,6 @@ async def test_does_not_restart_on_normal_exit(
309284
]
310285

311286

312-
async def test_does_not_restart_on_base_exception(
313-
actor_auto_restart_once: None, # pylint: disable=unused-argument
314-
caplog: pytest.LogCaptureFixture,
315-
) -> None:
316-
"""Create a faulty actor and expect it not to restart because it raises a base exception."""
317-
caplog.set_level("DEBUG", logger="frequenz.sdk.actor._actor")
318-
caplog.set_level("DEBUG", logger="frequenz.sdk.actor._run_utils")
319-
320-
channel: Broadcast[int] = Broadcast(name="channel")
321-
322-
actor = RaiseBaseExceptionActor(channel.new_receiver())
323-
324-
async with asyncio.timeout(1.0):
325-
await channel.new_sender().send(1)
326-
# We can't use pytest.raises() here because known BaseExceptions are handled
327-
# specially by pytest.
328-
try:
329-
await run(actor)
330-
except MyBaseException as error:
331-
assert str(error) == "This is a test"
332-
333-
assert BaseTestActor.restart_count == 0
334-
assert caplog.record_tuples == [
335-
(*RUN_INFO, "Starting 1 actor(s)..."),
336-
(*RUN_INFO, "Actor RaiseBaseExceptionActor[test]: Starting..."),
337-
(*ACTOR_INFO, "Actor RaiseBaseExceptionActor[test]: Started."),
338-
(*ACTOR_ERROR, "Actor RaiseBaseExceptionActor[test]: Raised a BaseException."),
339-
(
340-
*RUN_ERROR,
341-
"Actor RaiseBaseExceptionActor[test]: Raised an exception while running.",
342-
),
343-
(*RUN_INFO, "All 1 actor(s) finished."),
344-
]
345-
346-
347287
async def test_does_not_restart_if_cancelled(
348288
actor_auto_restart_once: None, # pylint: disable=unused-argument
349289
caplog: pytest.LogCaptureFixture,
@@ -390,6 +330,6 @@ async def cancel_actor() -> None:
390330
(*RUN_INFO, "Actor EchoActor[EchoActor]: Starting..."),
391331
(*ACTOR_INFO, "Actor EchoActor[EchoActor]: Started."),
392332
(*ACTOR_INFO, "Actor EchoActor[EchoActor]: Cancelled."),
393-
(*RUN_ERROR, "Actor EchoActor[EchoActor]: Raised an exception while running."),
333+
(*RUN_INFO, "Actor EchoActor[EchoActor]: Cancelled while running."),
394334
(*RUN_INFO, "All 1 actor(s) finished."),
395335
]

0 commit comments

Comments
 (0)