Skip to content

Commit e39f292

Browse files
committed
Use the task creator object to create tasks in the Service class
This change adds a new parameter to the Service class, `task_creator`, that will be used to create tasks. This is useful to allow the user to use different task creators, like the `asyncio` module or a `TaskGroup`. A method to create tasks was also added to the Service class, that uses the task creator object to create the tasks, hopefully making it easier for users not to forget to register tasks managed by the service in the `self._tasks` set. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent eb0848f commit e39f292

File tree

1 file changed

+45
-4
lines changed

1 file changed

+45
-4
lines changed

src/frequenz/core/asyncio.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ class Service(abc.ABC):
8888
8989
To implement a service, subclasses must implement the
9090
[`start()`][frequenz.core.asyncio.Service.start] method, which should start the
91-
background tasks needed by the service, and add them to the `_tasks` protected
92-
attribute.
91+
background tasks needed by the service using the
92+
[`create_task()`][frequezn.core.asyncio.Service.create_task] method.
9393
9494
If you need to collect results or handle exceptions of the tasks when stopping the
9595
service, then you need to also override the
@@ -114,7 +114,9 @@ def __init__(self, resolution_s: float, *, unique_id: str | None = None) -> None
114114
self._resolution_s = resolution_s
115115
116116
def start(self) -> None:
117-
self._tasks.add(asyncio.create_task(self._tick()))
117+
# Managed tasks are automatically saved, so there is no need to hold a
118+
# reference to them if you don't need to further interact with them.
119+
self.create_task(self._tick())
118120
119121
async def _tick(self) -> None:
120122
while True:
@@ -136,19 +138,25 @@ async def main() -> None:
136138
```
137139
"""
138140

139-
def __init__(self, *, unique_id: str | None = None) -> None:
141+
def __init__(
142+
self, *, unique_id: str | None = None, task_creator: TaskCreator = asyncio
143+
) -> None:
140144
"""Initialize this Service.
141145
142146
Args:
143147
unique_id: The string to uniquely identify this service instance.
144148
If `None`, a string based on `hex(id(self))` will be used. This is
145149
used in `__repr__` and `__str__` methods, mainly for debugging
146150
purposes, to identify a particular instance of a service.
151+
task_creator: The object that will be used to create tasks. Usually one of:
152+
the [`asyncio`]() module, an [`asyncio.AbstractEventLoop`]() or
153+
an [`asyncio.TaskGroup`]().
147154
"""
148155
# [2:] is used to remove the '0x' prefix from the hex representation of the id,
149156
# as it doesn't add any uniqueness to the string.
150157
self._unique_id: str = hex(id(self))[2:] if unique_id is None else unique_id
151158
self._tasks: set[asyncio.Task[Any]] = set()
159+
self._task_creator: TaskCreator = task_creator
152160

153161
@abc.abstractmethod
154162
def start(self) -> None:
@@ -180,6 +188,39 @@ def is_running(self) -> bool:
180188
"""
181189
return any(not task.done() for task in self._tasks)
182190

191+
def create_task(
192+
self,
193+
coro: collections.abc.Coroutine[Any, Any, TaskReturnT],
194+
*,
195+
name: str | None = None,
196+
context: contextvars.Context | None = None,
197+
) -> asyncio.Task[TaskReturnT]:
198+
"""Start a managed task.
199+
200+
A reference to the task will be held by the service, so there is no need to save
201+
the task object.
202+
203+
Tasks can be retrieved via the [`tasks`][frequenz.core.asyncio.Service.tasks]
204+
property.
205+
206+
Tasks created this way will also be automatically cancelled when calling
207+
[`cancel()`][frequenz.core.asyncio.Service.cancel] or
208+
[`stop()`][frequenz.core.asyncio.Service.stop], or when the service is used as
209+
a async context manager.
210+
211+
Args:
212+
coro: The coroutine to be managed.
213+
name: The name of the task.
214+
context: The context to be used for the task.
215+
216+
Returns:
217+
The new task.
218+
"""
219+
task = self._task_creator.create_task(coro, name=name, context=context)
220+
self._tasks.add(task)
221+
task.add_done_callback(self._tasks.discard)
222+
return task
223+
183224
def cancel(self, msg: str | None = None) -> None:
184225
"""Cancel all running tasks spawned by this service.
185226

0 commit comments

Comments
 (0)