Skip to content

Commit 0596041

Browse files
committed
Add Lifecycle, Communication and Implementing an Actor sections
These new sections explain in more detail how actors work, and what's needed to both use them and implement them properly. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent a9c5779 commit 0596041

File tree

1 file changed

+153
-3
lines changed

1 file changed

+153
-3
lines changed

src/frequenz/sdk/actor/__init__.py

Lines changed: 153 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,157 @@
4343
[`asyncio`][]. You can create and manage tasks within the actor, and the `Actor`
4444
class handles task cancellation and cleanup.
4545
46-
- **Simplified lifecycle management:** Actors are [async context
47-
managers](https://docs.python.org/3/reference/datamodel.html#async-context-managers)
48-
and also a [`run()`][frequenz.sdk.actor.run] function is provided.
46+
- **Simplified lifecycle management:** Actors are [async context managers] and also
47+
a [`run()`][frequenz.sdk.actor.run] function is provided.
48+
49+
## Lifecycle
50+
51+
Actors are not started when they are created. There are 3 main ways to start an actor
52+
(from most to least recommended):
53+
54+
1. By using the [`run()`][frequenz.sdk.actor.run] function.
55+
2. By using the actor as an [async context manager].
56+
3. By using the [`start()`][frequenz.sdk.actor.Actor.start] method.
57+
58+
!!! warning
59+
60+
1. If an actor raises an unhandled exception, it will be restarted automatically.
61+
62+
2. Actors manage [`asyncio.Task`][] objects, so a reference to the actor object must
63+
be held for as long as the actor is expected to be running, otherwise its tasks
64+
will be cancelled and the actor will stop. For more information, please refer to
65+
the [Python `asyncio`
66+
documentation](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task).
67+
68+
69+
### The `run()` Function
70+
71+
The [`run()`][frequenz.sdk.actor.run] function can start many actors at once and waits
72+
for them to finish. If any of the actors are stopped with errors, the errors will be
73+
logged.
74+
75+
???+ example
76+
77+
```python
78+
from frequenz.sdk.actor import Actor, run
79+
80+
class MyActor(Actor):
81+
async def _run(self) -> None:
82+
print("Hello World!")
83+
84+
await run(MyActor()) # (1)!
85+
```
86+
87+
1. Will block until the actor is stopped.
88+
89+
### Async Context Manager
90+
91+
When an actor is used as an [async context manager], it is started when the
92+
`async with` block is entered and stopped automatically when the block is exited
93+
(even if an exception is raised).
94+
95+
???+ example
96+
97+
```python
98+
from frequenz.sdk.actor import Actor
99+
100+
class MyActor(Actor):
101+
async def _run(self) -> None:
102+
print("Hello World!")
103+
104+
async with MyActor() as actor: # (1)!
105+
print("The actor is running")
106+
# (2)!
107+
```
108+
109+
1. [`start()`][frequenz.sdk.actor.Actor.start] is called automatically when entering
110+
the `async with` block.
111+
2. [`stop()`][frequenz.sdk.actor.Actor.stop] is called automatically when exiting
112+
the `async with` block.
113+
114+
### The `start()` Method
115+
116+
When using the [`start()`][frequenz.sdk.actor.Actor.start] method, the actor is
117+
started in the background and the method returns immediately. The actor will
118+
continue to run until it is **manually** stopped.
119+
120+
???+ example
121+
122+
```python
123+
from frequenz.sdk.actor import Actor
124+
125+
class MyActor(Actor):
126+
async def _run(self) -> None:
127+
print("Hello World!")
128+
129+
actor = MyActor() # (1)!
130+
actor.start() # (2)!
131+
print("The actor is running") # (3)!
132+
await actor.stop() # (4)!
133+
```
134+
135+
1. The actor is created but not started yet.
136+
2. The actor is started manually, it keeps running in the background.
137+
3. !!! danger
138+
139+
If this function would raise an exception, the actor will never be stopped!
140+
141+
4. Until the actor is stopped manually.
142+
143+
!!! warning
144+
145+
This method is not recommended because it is easy to forget to stop the actor
146+
manually, specially in error conditions.
147+
148+
## Communication
149+
150+
The [`Actor`][frequenz.sdk.actor.Actor] class doesn't impose any specific way to
151+
communicate between actors. However, [Frequenz
152+
Channels](https://github.com/frequenz-floss/frequenz-channels-python/) are always used
153+
as the communication mechanism between actors in the SDK.
154+
155+
## Implementing an Actor
156+
157+
To implement an actor, you must inherit from the [`Actor`][frequenz.sdk.actor.Actor]
158+
class and implement the abstract `_run()` method.
159+
160+
### The `_run()` Method
161+
162+
The abstract `_run()` method is called automatically by the base class when the actor is
163+
[started][frequenz.sdk.actor.Actor.start].
164+
165+
Normally an actor should run forever (or until it is
166+
[stopped][frequenz.sdk.actor.Actor.stop]), so it is very common to have an infinite loop
167+
in the `_run()` method, typically receiving messages from one or more channels
168+
([receivers][frequenz.channels.Receiver]), processing them and sending the results to
169+
other channels ([senders][frequenz.channels.Sender]).
170+
171+
### Stopping
172+
173+
By default, the [`stop()`][frequenz.sdk.actor.Actor.stop] method will call the
174+
[`cancel()`][frequenz.sdk.actor.Actor.cancel] method (which will cancel all the tasks
175+
created by the actor) and will wait for them to finish.
176+
177+
This means that when an actor is stopped, the `_run()` method will receive
178+
a [`CancelledError`][asyncio.CancelledError] exception. You should have this in mind
179+
when implementing your actor and make sure to handle this exception properly if you need
180+
to do any cleanup.
181+
182+
The actor will handle the [`CancelledError`][asyncio.CancelledError] exception
183+
automatically if it is not handled in the `_run()` method, so if there is no need for
184+
extra cleanup, you don't need to worry about it.
185+
186+
If an unhandled exception is raised in the `_run()` method, the actor will re-run the
187+
`_run()` method automatically. This ensures robustness in the face of errors, but you
188+
should also have this in mind if you need to do any cleanup to make sure the re-run
189+
doesn't cause any problems.
190+
191+
???+ tip
192+
193+
You could implement your own [`stop()`][frequenz.sdk.actor.Actor.stop] method to,
194+
for example, send a message to a channel to notify that the actor should stop, or
195+
any other more graceful way to stop the actor if you need to make sure it can't be
196+
interrupted at any `await` point.
49197
50198
## Example
51199
@@ -157,6 +305,8 @@ async def main() -> None: # (2)!
157305
158306
11. The actors are stopped and cleaned up automatically when the `async with`
159307
block ends.
308+
309+
[async context manager]: https://docs.python.org/3/reference/datamodel.html#async-context-managers
160310
"""
161311

162312
from ..timeseries._resampling import ResamplerConfig

0 commit comments

Comments
 (0)