Skip to content
This repository has been archived by the owner on Jun 7, 2023. It is now read-only.

Gavin/sta 3984 blog post on what is the actor model and when should i use #131

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
title: What is the Actor Model and When Should I Use It?
gavination marked this conversation as resolved.
Show resolved Hide resolved
description: "Defines the Actor Model, its use cases, discusses using it with statecharts"
tags:
- actor model
- modeling
- statechart
- tutorials
- state machine
- typescript
- blog
author: Gavin Bauman
publishedAt: 2023-5-30
---

At Stately, the [Actor Model](https://en.wikipedia.org/wiki/Actor_model) is one of our favorite programming paradigms, and we think it’s for good reason! The actor model allows developers to build reliable message-based systems by using _actors_ to communicate. This works extremely well with state machines and statecharts, which can also be modeled as actors and can communicate much in the same ways. Read on to learn what the actor model is, the problems it seeks to solve, and how you can use it in your projects to communicate reliably across different entities.

## What is the actor model?

The actor model has been around for quite a while, dating back to the 1970’s. That it’s used with frameworks like [Akka](https://akka.io/) and built natively into languages like Erlang(https://www.erlang.org/) are testaments to its utility. When researching the actor model, it’s very common to see the phrase “everything is an actor”, as it is a core tenant in the actor model philosophy. This simply means that in a given system, the _actor_ is the core unit of execution. Every action that occurs in the system is driven by an actor. An actor can communicate with other actors with the use of messaging, and they can also interact with external systems. Specifically, an actor can perform the following basic tasks:
gavination marked this conversation as resolved.
Show resolved Hide resolved

- Actors can spawn other actors
- Actors can send messages to other actors
- Actors can manage their own internal state
gavination marked this conversation as resolved.
Show resolved Hide resolved

It may sound simple, but this programming model allows for the development of highly scalable and concurrent systems. There are constraints though, the most important of which is that an actor _cannot_ modify the internal state of another actor directly. This can be done implicitly with messaging (i.e an actor updating its state in response to a message) but _never directly_.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can argue message passing is explicitly asking for a state change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can argue message passing is explicitly asking for a state change.

Hmm, messages don't always result in a state change though, nor is that the concern of the message sender. When a message is received, effects may be executed as a result, but those effects don't need to result in a state change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I was emphasizing on the "explicit vs implicit".


## When should I use it?

The actor model is extremely useful for divvying work that can be processed in parallel. It’s excellent for “fan-out/fan-in” scenarios where several functions need to be run at once and have their results combined before final processing. It also works well with building pub/sub systems, where several actors can be “workers”, waiting for messages from the “publisher”. Our last example, but certainly not the least, is in the case of systems that need to manage several similar entities, like a multiplayer game where every player is represented as an actor. A good rule of thumb is to at least consider the actor model whenever distribution and concurrency are core requirements.
gavination marked this conversation as resolved.
Show resolved Hide resolved
laurakalbag marked this conversation as resolved.
Show resolved Hide resolved

## When not to use it

As with any pattern, it’s just as important to understand its weaknesses as it is to understand its strengths. Typically you may not want to use the actor pattern when order really matters. Order is usually not promised in the actor pattern, and if one of the actors fails, you’ll have to deal with the concern of rolling back events. The actor model is often unnecessary when dealing with synchronous problems as well, and can add unnecessary overhead. Additionally, error handling can be tricky with actors. Erlang popularized the “let it crash” philosophy, but given the problem, it may not always be the most reasonable answer.

## How does Stately work with the actor model?
gavination marked this conversation as resolved.
Show resolved Hide resolved

With XState, we expose the ability to create instances of machines as actors! Looking at the example below, we can see that after defining our machine and its attributes, we only need to use `interpret()` to instantiate an actor (called `toggleActor`) and send it messages.

```typescript
import { createMachine, interpret } from "xstate";

// State machine definition
const toggleMachine = createMachine({
id: "toggle",
initial: "inactive",
states: {
inactive: { on: { TOGGLE: "active" } },
active: { on: { TOGGLE: "inactive" } },
},
});

// Machine instance with internal state
const toggleActor = interpret(toggleMachine);
toggleActor.subscribe((state) => console.log(state.value));
gavination marked this conversation as resolved.
Show resolved Hide resolved
toggleActor.start();
// => logs 'inactive'

toggleActor.send({ type: "TOGGLE" });
// => logs 'active'

toggleActor.send({ type: "TOGGLE" });
// => logs 'inactive'
```

This actor has its own state and context, and it can be updated whenever it receives events. Of course, in order for the internal state of an actor to be updated, the event must be a legal transition defined with the machine.

### Spawning Actors

XState can also be used to spawn other actors and communicate with each other:

```typescript
import { createMachine, spawn } from "xstate";
import { todoMachine } from "./todoMachine";

const todosMachine = createMachine({
// ...
on: {
"NEW_TODO.ADD": {
actions: assign({
todos: (context, event) => [
...context.todos,
{
todo: event.todo,
// add a new todoMachine actor with a unique name
ref: spawn(todoMachine, `todo-${event.id}`, { sync: true }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid mentioning sync: true, this is going away in v5

},
],
}),
},
// ...
},
});
Comment on lines +76 to +93
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const todosMachine = createMachine({
// ...
on: {
"NEW_TODO.ADD": {
actions: assign({
todos: (context, event) => [
...context.todos,
{
todo: event.todo,
// add a new todoMachine actor with a unique name
ref: spawn(todoMachine, `todo-${event.id}`, { sync: true }),
},
],
}),
},
// ...
},
});
const todosMachine = createMachine({
// ...
on: {
"NEW_TODO.ADD": {
actions: assign({
todos: (context, event) => [
...context.todos,
// add a new todoMachine actor with a unique name
spawn(todoMachine, `todo-${event.id}`),
],
}),
},
// ...
},
});

```

With the use of `spawn()` and `assign()`, we create a new actor instance when provided the machine and a unique identifier. By their nature, actions are ["fire and forget" effects](https://xstate.js.org/docs/guides/actions.html#api), meaning they are actions that are executed with no expectation of receiving events back to the actor. This makes sense for creating a new actor, but we may still want the parent actor to have a reference to its child, so we save that in its context using `assign()`. `spawn()` is the function called that actually creates the new actor. `spawn()` also sports another useful feature in that it accepts an `options` object with an optional `sync` property. Setting this to `true` allows the parent actor to subscribe to the child’s current state. The parent can access this state easily by calling `getSnapshot()` on the reference to the child.
gavination marked this conversation as resolved.
Show resolved Hide resolved

For more detailed examples around working with actors in XState, like callback or promised-based actor spawning, sending updates, and communicating between actors, [check out the guides in the XState documentation](https://xstate.js.org/docs/guides/actors.html).
gavination marked this conversation as resolved.
Show resolved Hide resolved