Skip to content

Commit 1a12981

Browse files
committed
Add an introduction to actors
This document introduces the actor programming model and how to use it in Frequenz SDK. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent d3be8e2 commit 1a12981

File tree

1 file changed

+148
-0
lines changed

1 file changed

+148
-0
lines changed

docs/intro/actors.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Actors
2+
3+
## Actor Programming Model
4+
5+
From [Wikipedia](https://en.wikipedia.org/wiki/Actor_model):
6+
7+
> The actor model in computer science is a mathematical model of concurrent
8+
> computation that treats an actor as the basic building block of concurrent
9+
> computation. In response to a message it receives, an actor can: make local
10+
> decisions, create more actors, send more messages, and determine how to
11+
> respond to the next message received. Actors may modify their own private
12+
> state, but can only affect each other indirectly through messaging (removing
13+
> the need for lock-based synchronization).
14+
15+
We won't get into much more detail here because it is outside the scope of this
16+
documentation. However, if you are interested in learning more about the actor
17+
programming model, here are some useful resources:
18+
19+
- [Actor Model (Wikipedia)](https://en.wikipedia.org/wiki/Actor_model)
20+
- [How the Actor Model Meets the Needs of Modern, Distributed Systems
21+
(Akka)](https://doc.akka.io/docs/akka/current/typed/guide/actors-intro.html)
22+
23+
## Frequenz SDK Actors
24+
25+
The [`Actor`][frequenz.sdk.actor.Actor] class serves as the foundation for
26+
creating concurrent tasks and all actors in the SDK inherit from it. This class
27+
provides a straightforward way to implement actors. It shares similarities with
28+
the traditional actor programming model but also has some unique features:
29+
30+
- **Message Passing:** Like traditional actors, our Actor class communicates
31+
through message passing. To do that they use [channels][frequenz.channels]
32+
for communication.
33+
34+
- **Automatic Restart:** If an unhandled exception occurs in an actor's logic
35+
(`_run` method), the actor will be automatically restarted. This ensures
36+
robustness in the face of errors.
37+
38+
- **Simplified Task Management:** Actors manage asynchronous tasks using
39+
[`asyncio`][]. You can create and manage tasks within the actor, and the `Actor`
40+
class handles task cancellation and cleanup.
41+
42+
- **Simplified lifecycle management:** Actors are [async context
43+
managers](https://docs.python.org/3/reference/datamodel.html#async-context-managers)
44+
and also a [`run()`][frequenz.sdk.actor.run] function is provided.
45+
46+
## Example
47+
48+
Here's a simple example to demonstrate how to create two actors and connect
49+
them.
50+
51+
Please note the annotations in the code (like {{code_annotation_marker}}), they
52+
explain step-by-step what's going on in order of execution.
53+
54+
```python title="actors.py"
55+
import asyncio
56+
57+
from frequenz.channels import Broadcast, Receiver, Sender
58+
from frequenz.sdk.actor import Actor
59+
60+
class Actor1(Actor): # (1)!
61+
def __init__(
62+
self,
63+
recv: Receiver[str],
64+
output: Sender[str],
65+
) -> None:
66+
super().__init__()
67+
self._recv = recv
68+
self._output = output
69+
70+
async def _run(self) -> None:
71+
async for msg in self._recv:
72+
await self._output.send(f"Actor1 forwarding: {msg!r}") # (8)!
73+
74+
75+
class Actor2(Actor):
76+
def __init__(
77+
self,
78+
recv: Receiver[str],
79+
output: Sender[str],
80+
) -> None:
81+
super().__init__()
82+
self._recv = recv
83+
self._output = output
84+
85+
async def _run(self) -> None:
86+
async for msg in self._recv:
87+
await self._output.send(f"Actor2 forwarding: {msg!r}") # (9)!
88+
89+
90+
async def main() -> None: # (2)!
91+
# (4)!
92+
input_channel: Broadcast[str] = Broadcast("Input to Actor1")
93+
middle_channel: Broadcast[str] = Broadcast("Actor1 -> Actor2 stream")
94+
output_channel: Broadcast[str] = Broadcast("Actor2 output")
95+
96+
input_sender = input_channel.new_sender()
97+
output_receiver = output_channel.new_receiver()
98+
99+
async with ( # (5)!
100+
Actor1(input_channel.new_receiver(), middle_channel.new_sender()),
101+
Actor2(middle_channel.new_receiver(), output_channel.new_sender()),
102+
):
103+
await input_sender.send("Hello") # (6)!
104+
msg = await output_receiver.receive() # (7)!
105+
print(msg) # (10)!
106+
# (11)!
107+
108+
if __name__ == "__main__": # (3)!
109+
asyncio.run(main())
110+
```
111+
112+
1. We define 2 actors: `Actor1` and `Actor2` that will just forward a message
113+
from an input channel to an output channel, adding some text.
114+
115+
2. We define an async `main()` function with the main logic of our [asyncio][] program.
116+
117+
3. We start the `main()` function in the async loop using [`asyncio.run()`][asyncio.run].
118+
119+
4. We create a bunch of [broadcast][frequenz.channels.Broadcast]
120+
[channels][frequenz.channels] to connect our actors.
121+
122+
* `input_channel` is the input channel for `Actor1`.
123+
* `middle_channel` is the channel that connects `Actor1` and `Actor2`.
124+
* `output_channel` is the output channel for `Actor2`.
125+
126+
5. We create two actors and use them as async context managers, `Actor1` and
127+
`Actor2`, and connect them by creating new
128+
[senders][frequenz.channels.Sender] and
129+
[receivers][frequenz.channels.Receiver] from the channels.
130+
131+
6. We schedule the [sending][frequenz.channels.Sender.send] of the message
132+
`Hello` to `Actor1` via `input_channel`.
133+
134+
7. We [receive][frequenz.channels.Receiver.receive] (await) the response from
135+
`Actor2` via `output_channel`. Between this and the previous steps the
136+
`async` calls in the actors will be executed.
137+
138+
8. `Actor1` sends the re-formatted message (`Actor1 forwarding: Hello`) to
139+
`Actor2` via the `middle_channel`.
140+
141+
9. `Actor2` sends the re-formatted message (`Actor2 forwarding: "Actor1
142+
forwarding: 'Hello'"`) to the `output_channel`.
143+
144+
10. Finally, we print the received message, which will still be `Actor2
145+
forwarding: "Actor1 forwarding: 'Hello'"`.
146+
147+
11. The actors are stopped and cleaned up automatically when the `async with`
148+
block ends.

0 commit comments

Comments
 (0)