diff --git a/docs/_scripts/macros.py b/docs/_scripts/macros.py index 3ba14eb54..09b8f7fb9 100644 --- a/docs/_scripts/macros.py +++ b/docs/_scripts/macros.py @@ -8,27 +8,39 @@ from markdown.extensions import toc from mkdocs_macros import plugin as macros +_CODE_ANNOTATION_MARKER: str = ( + r'' + r'' + r'' + r"" + r"" +) -def define_env(env: macros.MacrosPlugin) -> None: - """Define the hook to create macro functions for use in Markdown. + +def _slugify(text: str) -> str: + """Slugify a text. Args: - env: The environment to define the macro functions in. + text: The text to slugify. + + Returns: + The slugified text. """ + # The type of the return value is not defined for the markdown library. + # Also for some reason `mypy` thinks the `toc` module doesn't have a + # `slugify_unicode` function, but it definitely does. + return toc.slugify_unicode(text, "-") # type: ignore[attr-defined,no-any-return] - def _slugify(text: str) -> str: - """Slugify a text. - Args: - text: The text to slugify. +def define_env(env: macros.MacrosPlugin) -> None: + """Define the hook to create macro functions for use in Markdown. - Returns: - The slugified text. - """ - # The type of the return value is not defined for the markdown library. - # Also for some reason `mypy` thinks the `toc` module doesn't have a - # `slugify_unicode` function, but it definitely does. - return toc.slugify_unicode(text, "-") # type: ignore[attr-defined,no-any-return] + Args: + env: The environment to define the macro functions in. + """ + # A variable to easily show an example code annotation from mkdocs-material. + # https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#adding-annotations + env.variables["code_annotation_marker"] = _CODE_ANNOTATION_MARKER @env.macro # type: ignore[misc] def glossary(term: str) -> str: diff --git a/docs/css/style.css b/docs/css/style.css index bbe472c87..ddcf65808 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -26,3 +26,13 @@ a.external:hover::after, a.md-nav__link[href^="https:"]:hover::after { .md-main__inner { margin-bottom: 1.5rem; } + +/* Code annotations with numbers + * https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#annotations-with-numbers + */ +.md-typeset .md-annotation__index > ::before { + content: attr(data-md-annotation-id); +} +.md-typeset :focus-within > .md-annotation__index > ::before { + transform: none; +} diff --git a/docs/intro/actors.md b/docs/intro/actors.md new file mode 100644 index 000000000..7dff7bb84 --- /dev/null +++ b/docs/intro/actors.md @@ -0,0 +1,148 @@ +# Actors + +## Actor Programming Model + +From [Wikipedia](https://en.wikipedia.org/wiki/Actor_model): + +> The actor model in computer science is a mathematical model of concurrent +> computation that treats an actor as the basic building block of concurrent +> computation. In response to a message it receives, an actor can: make local +> decisions, create more actors, send more messages, and determine how to +> respond to the next message received. Actors may modify their own private +> state, but can only affect each other indirectly through messaging (removing +> the need for lock-based synchronization). + +We won't get into much more detail here because it is outside the scope of this +documentation. However, if you are interested in learning more about the actor +programming model, here are some useful resources: + +- [Actor Model (Wikipedia)](https://en.wikipedia.org/wiki/Actor_model) +- [How the Actor Model Meets the Needs of Modern, Distributed Systems + (Akka)](https://doc.akka.io/docs/akka/current/typed/guide/actors-intro.html) + +## Frequenz SDK Actors + +The [`Actor`][frequenz.sdk.actor.Actor] class serves as the foundation for +creating concurrent tasks and all actors in the SDK inherit from it. This class +provides a straightforward way to implement actors. It shares similarities with +the traditional actor programming model but also has some unique features: + +- **Message Passing:** Like traditional actors, our Actor class communicates + through message passing. To do that they use [channels][frequenz.channels] + for communication. + +- **Automatic Restart:** If an unhandled exception occurs in an actor's logic + (`_run` method), the actor will be automatically restarted. This ensures + robustness in the face of errors. + +- **Simplified Task Management:** Actors manage asynchronous tasks using + [`asyncio`][]. You can create and manage tasks within the actor, and the `Actor` + class handles task cancellation and cleanup. + +- **Simplified lifecycle management:** Actors are [async context + managers](https://docs.python.org/3/reference/datamodel.html#async-context-managers) + and also a [`run()`][frequenz.sdk.actor.run] function is provided. + +## Example + +Here's a simple example to demonstrate how to create two actors and connect +them. + +Please note the annotations in the code (like {{code_annotation_marker}}), they +explain step-by-step what's going on in order of execution. + +```python title="actors.py" +import asyncio + +from frequenz.channels import Broadcast, Receiver, Sender +from frequenz.sdk.actor import Actor + +class Actor1(Actor): # (1)! + def __init__( + self, + recv: Receiver[str], + output: Sender[str], + ) -> None: + super().__init__() + self._recv = recv + self._output = output + + async def _run(self) -> None: + async for msg in self._recv: + await self._output.send(f"Actor1 forwarding: {msg!r}") # (8)! + + +class Actor2(Actor): + def __init__( + self, + recv: Receiver[str], + output: Sender[str], + ) -> None: + super().__init__() + self._recv = recv + self._output = output + + async def _run(self) -> None: + async for msg in self._recv: + await self._output.send(f"Actor2 forwarding: {msg!r}") # (9)! + + +async def main() -> None: # (2)! + # (4)! + input_channel: Broadcast[str] = Broadcast("Input to Actor1") + middle_channel: Broadcast[str] = Broadcast("Actor1 -> Actor2 stream") + output_channel: Broadcast[str] = Broadcast("Actor2 output") + + input_sender = input_channel.new_sender() + output_receiver = output_channel.new_receiver() + + async with ( # (5)! + Actor1(input_channel.new_receiver(), middle_channel.new_sender()), + Actor2(middle_channel.new_receiver(), output_channel.new_sender()), + ): + await input_sender.send("Hello") # (6)! + msg = await output_receiver.receive() # (7)! + print(msg) # (10)! + # (11)! + +if __name__ == "__main__": # (3)! + asyncio.run(main()) +``` + +1. We define 2 actors: `Actor1` and `Actor2` that will just forward a message + from an input channel to an output channel, adding some text. + +2. We define an async `main()` function with the main logic of our [asyncio][] program. + +3. We start the `main()` function in the async loop using [`asyncio.run()`][asyncio.run]. + +4. We create a bunch of [broadcast][frequenz.channels.Broadcast] + [channels][frequenz.channels] to connect our actors. + + * `input_channel` is the input channel for `Actor1`. + * `middle_channel` is the channel that connects `Actor1` and `Actor2`. + * `output_channel` is the output channel for `Actor2`. + +5. We create two actors and use them as async context managers, `Actor1` and + `Actor2`, and connect them by creating new + [senders][frequenz.channels.Sender] and + [receivers][frequenz.channels.Receiver] from the channels. + +6. We schedule the [sending][frequenz.channels.Sender.send] of the message + `Hello` to `Actor1` via `input_channel`. + +7. We [receive][frequenz.channels.Receiver.receive] (await) the response from + `Actor2` via `output_channel`. Between this and the previous steps the + `async` calls in the actors will be executed. + +8. `Actor1` sends the re-formatted message (`Actor1 forwarding: Hello`) to + `Actor2` via the `middle_channel`. + +9. `Actor2` sends the re-formatted message (`Actor2 forwarding: "Actor1 + forwarding: 'Hello'"`) to the `output_channel`. + +10. Finally, we print the received message, which will still be `Actor2 + forwarding: "Actor1 forwarding: 'Hello'"`. + +11. The actors are stopped and cleaned up automatically when the `async with` + block ends.