Skip to content

Commit a51e816

Browse files
authored
Move Actors documentation to the actor module (#688)
Having the documentation in the module instead of the documentation page makes it easier to keep the documentation up to date and also allows us to reuse the documentation, so using consulting the API reference can also get a high-level overview of the module.
2 parents c7c787a + e44f103 commit a51e816

File tree

2 files changed

+157
-147
lines changed

2 files changed

+157
-147
lines changed

docs/intro/actors.md

Lines changed: 7 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,9 @@
11
# Actors
22

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.
3+
::: frequenz.sdk.actor
4+
options:
5+
members: []
6+
show_bases: false
7+
show_root_heading: false
8+
show_root_toc_entry: false
9+
show_source: false

src/frequenz/sdk/actor/__init__.py

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,156 @@
11
# License: MIT
22
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
33

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

6155
from ..timeseries._resampling import ResamplerConfig
7156
from ._actor import Actor

0 commit comments

Comments
 (0)