@@ -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