|
43 | 43 | [`asyncio`][]. You can create and manage tasks within the actor, and the `Actor` |
44 | 44 | class handles task cancellation and cleanup. |
45 | 45 |
|
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. |
49 | 197 |
|
50 | 198 | ## Example |
51 | 199 |
|
@@ -157,6 +305,8 @@ async def main() -> None: # (2)! |
157 | 305 |
|
158 | 306 | 11. The actors are stopped and cleaned up automatically when the `async with` |
159 | 307 | block ends. |
| 308 | +
|
| 309 | +[async context manager]: https://docs.python.org/3/reference/datamodel.html#async-context-managers |
160 | 310 | """ |
161 | 311 |
|
162 | 312 | from ..timeseries._resampling import ResamplerConfig |
|
0 commit comments