Skip to content

Commit de2dff6

Browse files
committed
Document and fix how to implement custom stopping logic
The `BackgroundService` should not return early if there are no tasks in `stop()`, as sub-classes could need to implement a custom stopping logic. In this case it should be enough to override the `cancel()` and `wait()` methods, and the `is_running` property. We also add an example of how this could be done. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 6c5e4ce commit de2dff6

File tree

2 files changed

+43
-4
lines changed

2 files changed

+43
-4
lines changed

RELEASE_NOTES.md

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

4040
## Bug Fixes
4141

42-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
42+
- Fix a bug in `BackgroundService` where it won't try to `self.cancel()` and `await self.wait()` if there are no internal tasks. This prevented to properly implement custom stop logic without having to redefine the `stop()` method too.

src/frequenz/sdk/actor/_background_service.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ class BackgroundService(abc.ABC):
3636
information, please refer to the [Python `asyncio`
3737
documentation](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task).
3838
39-
Example:
39+
Example: Simple background service example
4040
```python
41+
from typing_extensions import override
4142
import datetime
4243
import asyncio
4344
@@ -46,6 +47,7 @@ def __init__(self, resolution_s: float, *, name: str | None = None) -> None:
4647
super().__init__(name=name)
4748
self._resolution_s = resolution_s
4849
50+
@override
4951
def start(self) -> None:
5052
self._tasks.add(asyncio.create_task(self._tick()))
5153
@@ -67,6 +69,45 @@ async def main() -> None:
6769
6870
asyncio.run(main())
6971
```
72+
73+
Example: Background service example using custom stopping logic
74+
If you need to implement custom stopping logic, you can override the
75+
[`cancel()`][frequenz.sdk.actor.BackgroundService.cancel] and
76+
[`wait()`][frequenz.sdk.actor.BackgroundService.wait] methods, and the
77+
[`is_running`][frequenz.sdk.actor.BackgroundService.is_running] property.
78+
79+
For example, if you are using an external library that uses tasks internally and
80+
you don't have access to them.
81+
82+
```python
83+
from typing_extensions import override
84+
import asyncio
85+
86+
class SomeService(BackgroundService):
87+
def __init__(self, somelib, *, name: str | None = None) -> None:
88+
self.somelib = somelib
89+
super().__init__(name=name)
90+
91+
@override
92+
def start(self) -> None:
93+
self.somelib.start()
94+
95+
@property
96+
@override
97+
def is_running(self) -> bool:
98+
return self.somelib.is_running()
99+
100+
@override
101+
def cancel(self, msg: str | None = None) -> None:
102+
self.somelib.cancel()
103+
104+
@override
105+
async def wait(self) -> None:
106+
try:
107+
await self.somelib.wait()
108+
except BaseExceptionGroup as exc:
109+
raise BaseExceptionGroup("Error while stopping SomeService", [exc]) from exc
110+
```
70111
"""
71112

72113
def __init__(self, *, name: str | None = None) -> None:
@@ -144,8 +185,6 @@ async def stop(self, msg: str | None = None) -> None: # noqa: DOC503
144185
BaseExceptionGroup: If any of the tasks spawned by this service raised an
145186
exception.
146187
"""
147-
if not self._tasks:
148-
return
149188
self.cancel(msg)
150189
try:
151190
await self.wait()

0 commit comments

Comments
 (0)