The actor model

๐ŸŽฌ behind the scenes

THAT Conference Texas 2024

David Khourshid ยท @davidkpiano

stately.ai

Setting the scene:

Dealing with complex logic

Flashback:

Origin story

A Universal Modular ACTOR Formalism for Artificial Intelligence (1973)

Carl Hewitt, Peter Bishop, Richard Steiger

Alan Kay

Gul Agha

  • Formalized the actor model

    Actors: A Model of Concurrent Computation in Distributed Systems

Alan Kay

Easier than
you may think

State machines ๐Ÿ˜ตโ€๐Ÿ’ซ

๐Ÿ‘ฉโ€๐Ÿ’ป

๐Ÿง”

๐Ÿ‘ฉโ€๐Ÿณ

โ˜•๏ธโ”

๐Ÿ“

๐Ÿ’ตโ“

๐Ÿ’ต

๐Ÿ“„โœ…

โ˜•๏ธ

โ˜•๏ธ

Acting:

The real world

๐Ÿ‘ฉโ€๐Ÿ’ป

๐Ÿง”

I would like a coffee...

What would you like?

ย Actorย 

I would like a coffee, please.

ย Actorย 

๐Ÿ’ญ

๐Ÿ’ฌ

๐Ÿ’ฌ

Here you go. โ˜•๏ธ

Thanks!

What is an actor?

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐‘“ (๐‘ฅ)

Behavior

State

๐ŸŽญ Change its state/behavior

A

B

โœ‰๏ธ

โœ‰๏ธ

C

โœ‰๏ธ

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

What is an actor?

A

โœ‰๏ธ

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

A โ†’ A'

What is an actor?

A

โœ‰๏ธ

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

B

โœ‰๏ธ

What is an actor?

A

โœ‰๏ธ

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

๐Ÿ†•

๐Ÿ†•

What is an actor?

An actor's script

  • ๐Ÿ“ฌ Send & receive messages
  • ๐ŸŽญ Change its internal state
  • ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Spawn child actors
{
  orders: [/* ... */],
  inventory: {
    // ...
  },
  sales: 134.65
}

An actor's script

Actor System

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

processing...

state ??

Right on queue:

Actor mailboxes

โœ‰๏ธ

โœ‰๏ธ

โœ‰๏ธ

โœ‰๏ธ

Right on queue:

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

โœ‰๏ธ

โœ‰๏ธ

Right on queue:

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

Right on queue:

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

state 3

โœ‰๏ธ

Right on queue:

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

state 3

โœ‰๏ธ

state 4

Right on queue:

Actor mailboxes

Actors backstage

Backend applications

Actors onstage

Frontend applications

Rehearsal time:ย 

Making an actor

function createActor(idk) {
  // ...

  return {
    send: (event) => {
      // ...
    }
  };
}

const actor = createActor();

actor.send({ type: 'inc' });
function createActor(idk) {
  let state = {
    count: 0
  };

  return {
    send: (event) => {
      // ...
      if (event.type === 'increment') {
        state.count++;
      }
    }
  };
}

const actor = createActor();

actor.send({ type: 'inc' });
function createActor(transition) {
  let state = {
    count: 0
  };

  return {
    send: (event) => {
      state = transition(state, event);
    }
  };
}

const actor = createActor(/* ... */);

actor.send({ type: 'inc' });
function createActor(logic) {
  let state = logic.initialState;

  return {
    send: (event) => {
      state = logic.transition(state, event);
    }
  };
}

const actor = createActor({
  logic: (state, event) => {/* ... */},
  initialState: {/* ... */}
});

actor.send({ type: 'inc' });

Don't call us,
we'll call you:

Subscribing to actors

function createActor(logic) {
  let state = logic.initialState;
  const observers = new Set();

  return {
    send: (event) => {
      state = logic.transition(state, event);
      observers.forEach((observer) => {
        observer.next(state);
      });
    },
    subscribe: (observer) => {
      observers.add(observer);

      return () => {
        observers.delete(observer);
      };
    }
  };
}

const actor = createActor({
  transition: (state, event) => {
    if (event.type === 'inc') {
      return { ...state, count: state.count + 1 };
    }
    return state;
  },
  initialState: { count: 0 }
});

actor.subscribe((s) => {
  console.log(s);
});

actor.send({ type: 'inc' });
// => { count: 1 }
function createActor(logic) {
  let state = {
    status: 'inactive',
    context: logic.initialContext
  };

  const observers = new Set();

  return {
    send: (event) => {
      state = logic.transition(state.context, event);
      observers.forEach((observer) => {
        observer.next(state);
      });
    },
    subscribe: (observer) => {
      observers.add(observer);

      return () => {
        observers.delete(observer);
      };
    },
    start: () => {
      state.status = 'active';
      observers.forEach((observer) => {
        observer.next(state);
      });
    }
  };
}

const actor = createActor({
  transition: (ctx, event) => {
    if (event.type === 'inc') {
      return { ...ctx, count: ctx.count + 1 };
    }
    return ctx;
  },
  initialContext: { count: 0 }
});

actor.subscribe((s) => {
  console.log(s);
});

actor.start();
// { context: { count: 0 } }

actor.send({ type: 'inc' });
// { context: { count: 1 } }

Special FX:

Managing side-effects

function createActor(logic) {
  let state = {
    status: 'inactive',
    context: logic.initialContext
  };

  const observers = new Set();

  const actor = {
    send: (event) => {
      state = logic.transition(
        state.context,
        event,
        actor
      );
      observers.forEach((observer) => {
        observer.next(state);
      });
    },
    subscribe: (observer) => {
      observers.add(observer);

      return () => {
        observers.delete(observer);
      };
    },
    start: () => {
      state.status = 'active';
      observers.forEach((observer) => {
        observer.next(state);
      });
    }
  };

  return actor;
}

const actor = createActor({
  transition: (ctx, event, self) => {
    if (event.type === 'load') {
      const promise = new Promise((res) => {
        setTimeout(() => {
          res({ name: 'David' });
        }, 1000);
      });

      promise.then((output) => {
        self.send({ type: 'resolve', data: output });
      });

      return ctx;
    } else if (event.type === 'resolve') {
      return {
        ...ctx,
        user: event.output
      };
    }
    return ctx;
  },
  initialContext: { user: null }
});

actor.subscribe((s) => {
  console.log(s);
});

actor.start();
// { context: { count: 0 } }

actor.send({ type: 'inc' });
// { context: { count: 1 } }

Guest appearance:

Actors in XState

npm i xstate

stately.ai/docs

import { createActor } from 'xstate';
import { someLogic } from './someLogic';

const actor = createActor(someLogic);

actor.subscribe((snapshot) => {
  console.log(snapshot);
});

actor.start();
import { fromTransition, createActor } from 'xstate';

const counterLogic = fromTransition(
  // Behavior
  (state, event) => {
    if (event.type === 'inc') {
      return {
        ...state,
        count: state.count + 1
      };
    }
    return state;
  },
  // Initial state
  { count: 0 }
);

const counterActor = createActor(counterLogic);
counterActor.subscribe(/* ... */);
counterActor.start();
counterActor.send({ type: 'inc' });
import { fromTransition, createActor } from 'xstate';

const counterLogic = fromTransition(
  // Behavior
  (state, event) => {
    if (event.type === 'inc') {
      return {
        ...state,
        count: state.count + 1
      };
    }
    return state;
  },
  // Initial state
  // with input
  ({ input }) => ({
    count: input.initialCount
  })
);

const counterActor = createActor(counterLogic, {
  input: {
    initialCount: 100
  }
});
counterActor.subscribe(/* ... */);
counterActor.start();
counterActor.send({ type: 'inc' });
import { fromPromise, createActor } from 'xstate';
import { fetchUser } from './fetchUser';

const promiseLogic = fromPromise(async ({ input }) => {
  const user = await fetchUser(input.userId);

  return user;
});

const promiseActor = createActor(promiseLogic, {
  input: { userId: 'user42' }
});

promiseActor.subscribe((s) => {
  if (s.status === 'done') {
    console.log(s.output);
  }
});

promiseActor.start();
import { setup, createActor } from 'xstate';

const counterMachine = setup({
  actors: {
    promiseLogic,
    counterLogic
  }
}).createMachine({
  initial: 'gettingUser',
  states: {
    gettingUser: {
      invoke: {
        src: 'promiseLogic',
        input: {/* ... */},
        onDone: {
          target: 'counting'
        }
      }
    },
    counting: {
      invoke: {
        id: 'counter',
        src: 'counterLogic',
        input: {
          initialCount: 0
        },
        onSnapshot: {
          target: 'reachedMaxCount',
          guard: ({ event }) => {
            return event.snapshot.context.count === 10;
          }
        }
      },
      on: {
        inc: {
          actions: sendTo('counter', { type: 'inc' })
        }
      }
    },
    reachedMaxCount: {
      type: 'final'
    }
  }
});

const counterActor = createActor(counterMachine);

counterActor.subscribe((s) => {
  console.log(s);
});

counterActor.start();

counterActor.send({ type: 'inc' });

Visual FX:

Visualizing actors

Stately Inspect: Demo

npm i @statelyai/inspect

import { createBrowserInspector } from '@statelyai/inspect';

const inspector = createBrowserInspector();

inspector.actor('speaker');

inspector.actor('listener');

inspector.event('speaker', 'question?', {
  source: 'listener'
});

inspector.event('listener', 'answer!', {
  source: 'speaker'
});

Streaming live:

Location transparency

Understudies:

Fault tolerance

Actor critics:

Pros and cons

  • Easy to scale
  • Fault tolerance
  • Location transparency
  • No shared state
  • Event-driven
  • "Microservice hell"
  • Learning curve
  • Indirection
  • Unfamiliarity

THAT Conference 2024

David Khourshid ยท @davidkpiano
stately.ai

End scene:

Final credits

THAT Conference 2024

David Khourshid ยท @davidkpiano
stately.ai

Improv time:

Any questions?

The actor model, behind the scenes

By David Khourshid

The actor model, behind the scenes

  • 172