Skip to content

Commit 9d1f3cd

Browse files
authored
docs: Add an introduction to actors (#679)
This new document introduces the actor programming model and how to use it in Frequenz SDK. Fixes #648.
2 parents 2c8f61f + 1a12981 commit 9d1f3cd

File tree

3 files changed

+184
-14
lines changed

3 files changed

+184
-14
lines changed

docs/_scripts/macros.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,39 @@
88
from markdown.extensions import toc
99
from mkdocs_macros import plugin as macros
1010

11+
_CODE_ANNOTATION_MARKER: str = (
12+
r'<span class="md-annotation">'
13+
r'<span class="md-annotation__index" tabindex="-1">'
14+
r'<span data-md-annotation-id="1"></span>'
15+
r"</span>"
16+
r"</span>"
17+
)
1118

12-
def define_env(env: macros.MacrosPlugin) -> None:
13-
"""Define the hook to create macro functions for use in Markdown.
19+
20+
def _slugify(text: str) -> str:
21+
"""Slugify a text.
1422
1523
Args:
16-
env: The environment to define the macro functions in.
24+
text: The text to slugify.
25+
26+
Returns:
27+
The slugified text.
1728
"""
29+
# The type of the return value is not defined for the markdown library.
30+
# Also for some reason `mypy` thinks the `toc` module doesn't have a
31+
# `slugify_unicode` function, but it definitely does.
32+
return toc.slugify_unicode(text, "-") # type: ignore[attr-defined,no-any-return]
1833

19-
def _slugify(text: str) -> str:
20-
"""Slugify a text.
2134

22-
Args:
23-
text: The text to slugify.
35+
def define_env(env: macros.MacrosPlugin) -> None:
36+
"""Define the hook to create macro functions for use in Markdown.
2437
25-
Returns:
26-
The slugified text.
27-
"""
28-
# The type of the return value is not defined for the markdown library.
29-
# Also for some reason `mypy` thinks the `toc` module doesn't have a
30-
# `slugify_unicode` function, but it definitely does.
31-
return toc.slugify_unicode(text, "-") # type: ignore[attr-defined,no-any-return]
38+
Args:
39+
env: The environment to define the macro functions in.
40+
"""
41+
# A variable to easily show an example code annotation from mkdocs-material.
42+
# https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#adding-annotations
43+
env.variables["code_annotation_marker"] = _CODE_ANNOTATION_MARKER
3244

3345
@env.macro # type: ignore[misc]
3446
def glossary(term: str) -> str:

docs/css/style.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,13 @@ a.external:hover::after, a.md-nav__link[href^="https:"]:hover::after {
2626
.md-main__inner {
2727
margin-bottom: 1.5rem;
2828
}
29+
30+
/* Code annotations with numbers
31+
* https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#annotations-with-numbers
32+
*/
33+
.md-typeset .md-annotation__index > ::before {
34+
content: attr(data-md-annotation-id);
35+
}
36+
.md-typeset :focus-within > .md-annotation__index > ::before {
37+
transform: none;
38+
}

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)