Preview of main at d0a501a, 2026-11-07

xactor is a header-only, deterministic actor system for C++20, one of the libraries of webcpp. Its actors handle one message at a time, on one thread, in an order that depends only on the order the messages were sent; every execution is paid from a budget of fuel; and time is a number the host brings.

This page teaches xactor concept by concept: why each exists, its smallest example, its variations and its rules. Every program it shows was compiled and run, natively and, but for the program built on Boost.Asio, for wasm32-wasip2 and wasm32-wasip3, and printed the output shown under it. The reference that closes it lists every name the headers declare.

Introduction

An actor is an independent object that reaches others only by messages: it has its own state, which only it changes, in response to a message it receives, and it handles one message at a time. So each actor can be understood on its own, and tested by the messages it takes and sends. xactor runs actors in that model, and promises a program what it can build on:

  • The same input makes the same run. The actors run on one thread, the host’s, and the order in which they receive their messages depends only on the order the messages were sent. Two runs of the same deliveries, under any of xactor’s drivers, deliver the same messages in the same order, and record the same envelope log to prove it.

  • Every execution ends. All the work that one message from the host causes is an execution, and an execution has a budget of fuel, from which every message its actors send is paid. No chain or storm of messages runs forever.

  • Time is the host’s. xactor reads no clock. An actor that must act later arms a timer, which fires when the host brings a time that reaches it, so a run with timers is as repeatable as one without.

The behaviour of an actor is C++ code. It can be dynamic, a class whose handle decides at run time what each message does, or static, a state machine compiled into the program, here with Boost.MSM (Static actors). A library can build on xactor too: webcpp’s xstate builds its actor layer on it.

Requirements

  • A C++20 compiler. xactor’s tests and examples are built and run natively, and for wasm32-wasip2 and wasm32-wasip3 with the Clang of wasi-sdk, under wasmtime (Testing).

  • Boost’s headers, 1.92 or newer. xactor uses Boost.System, for result and the error codes, and, outside a WASI build, Boost.Asio, for its Asio driver. It is header-only, and nothing of Boost is compiled for it.

  • webcpp’s configuration, config, whose <webcpp/config.hpp> every header of xactor includes first. It is header-only too.

  • Without exceptions, a definition of boost::throw_exception. xactor throws no exception of its own: it is built and tested with exceptions on, on every target, and its headers compile without them. Whether a program uses them is its own choice. Without them, Boost.System’s result still names boost::throw_exception, and the program defines it (Quick start). A program built with exceptions needs nothing here, since Boost defines the function then.

  • Nothing else: no threads, no clock, and no global or static mutable state.

Reading this page

One header, <webcpp/xactor.hpp>, includes the whole library, whose names are in the namespace webcpp::xactor. Each header of <webcpp/xactor/> may also be included alone. The code of this page writes the names as xactor::scheduler, as after namespace xactor = webcpp::xactor;, which every example declares.

The quick start builds a first program, and the model explains its parts: the messages, the logic of the actors, the scheduler that hosts them. Writing an actor logic and The scheduler take the parts a program writes and calls most, and Turns lists what an actor can do while it handles a message. The sections after it take what bounds and moves a run: fuel, timers, the lifecycle of an actor and its children, and the drivers that decide when turns happen. Static actors gives an actor a compile-time state machine for its behaviour, and Rules that keep a run deterministic says what an actor must not do, so that its runs stay the same. Testing shows how a program tests its actors, and how xactor itself is tested. Guarantees lists what xactor promises whatever its caller does, each promise under a number that the code and the tests cite. The reference and the glossary close the page.

xactor leaves to the program, or to a library built on it, what an actor system may add on top of it: a child’s end reaching its parent, a registry of actors by name, and the inspection of what each actor does. A child that must tell its parent that it ended sends it a message, to turn.parent(), in the turn it ends (Lifecycle).

Quick start

This program has two actors. A counter asks an echo for the numbers 1, 2 and 3, one message each. The echo answers each with the same number, and once the counter has its three answers, it finishes with their sum. The host, the code around the actors, creates both, sends the counter one message, and runs the actors until no message is left.

The program begins with xactor’s header and an alias for its namespace:

#include <webcpp/xactor.hpp>

#include <cstddef>
#include <iostream>
#include <variant>

namespace xactor = webcpp::xactor;

The messages are plain structs, and the message type of the actors is one std::variant of all of them. Every type of xactor that carries a message is a template over that variant:

struct start {};

struct ask {
    int value = 0;
};

struct answer {
    int value = 0;
};

struct total {
    int value = 0;
};

using message = std::variant<start, ask, answer, total>;

What an actor does is its logic: a class derived from xactor::actor_logic<message>, whose one handle is called with one message at a time. The message arrives in an envelope, cause, which holds it as cause.payload, and everything the actor does to other actors it does through its turn, turn. The echo replies to whoever asked. The counter is created with the echo’s actor_ref, the name of the actor it asks; on start it sends three asks, and it adds up the answers until it has three, when it finishes with the total:

/** Answers every ask with the same number. */
class echo final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        if (const auto* asked = std::get_if<ask>(&cause.payload)) {
            return turn.reply(answer{.value = asked->value});
        }
        return {};
    }
};

/** Asks the echo for 1, 2 and 3, and finishes with the sum of the answers. */
class counter final : public xactor::actor_logic<message> {
public:
    explicit counter(xactor::actor_ref echo) : echo_(echo) {}

    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        if (std::holds_alternative<start>(cause.payload)) {
            for (int value = 1; value <= 3; ++value) {
                const xactor::result<void> sent = turn.send(echo_, ask{.value = value});
                if (!sent.has_value()) {
                    return sent;
                }
            }
            return {};
        }
        if (const auto* answered = std::get_if<answer>(&cause.payload)) {
            sum_ += answered->value;
            ++answers_;
            if (answers_ == 3) {
                return turn.finish(total{.value = sum_});
            }
        }
        return {};
    }

private:
    xactor::actor_ref echo_;
    int sum_ = 0;
    int answers_ = 0;
};

The host creates a scheduler, the system that holds the actors, with a budget of 100 units of fuel for each execution. It creates the two actors with create_actor, which constructs a logic from the arguments after the scheduler and returns the new actor’s actor_ref. It delivers start to the counter in execution 1, and runs a fifo_driver until no message is left. Then it reads what the counter finished with, and the fuel execution 1 has left:

xactor::scheduler<message> system(xactor::budgets{.fuel = 100});
const xactor::result<xactor::actor_ref> echoing = xactor::create_actor<echo>(system);
if (!echoing.has_value()) {
    return 1;
}
const xactor::result<xactor::actor_ref> counting =
    xactor::create_actor<counter>(system, *echoing);
if (!counting.has_value()) {
    return 1;
}

const xactor::correlation_id execution{.value = 1};
if (!system.deliver(*counting, start{}, execution).has_value()) {
    return 1;
}
xactor::fifo_driver<message> driver(system);
const xactor::result<std::size_t> delivered = driver.run_until_idle(nullptr);
if (!delivered.has_value()) {
    return 1;
}

const message* output = system.output_of(*counting);
if (output == nullptr) {
    return 1;
}
std::cout << "delivered " << *delivered << " messages\n";
std::cout << "total " << std::get<total>(*output).value << ", fuel left "
          << system.fuel_of(execution) << '\n';

It prints:

delivered 7 messages
total 6, fuel left 94

Seven messages were delivered: the host’s start, three asks and three answers. Each ask the counter sent and each answer the echo replied cost one unit of fuel, and the host’s start cost nothing, so execution 1 has 94 of its 100 units left. Every call that can fail returns a result, and the program checks each one: xactor reports a failure as a value, and throws no exception of its own (guarantee 20).

Building it

xactor needs Boost’s headers and webcpp’s configuration, libs/config, and nothing compiled. From the root of a webcpp checkout, with the program copied as quick_start.cpp, this command builds it:

clang++ -std=c++20 -I libs/xactor/include -I libs/config/include quick_start.cpp \
    -o quick_start

A compiler that does not find Boost on its default include path is given Boost’s include directory with -isystem: for Homebrew’s Boost, the one under /opt/homebrew/opt/boost. Without exceptions, the program also needs a definition of boost::throw_exception, which Boost.System’s result names: webcpp’s tools/throw_exception.cpp reports what was thrown on the standard error and aborts, since in such a build a throw is a defect, never a condition.

clang++ -std=c++20 -fno-exceptions -DBOOST_NO_EXCEPTIONS -I libs/xactor/include \
    -I libs/config/include quick_start.cpp tools/throw_exception.cpp -o quick_start

In the checkout, b2 libs/xactor/example builds and runs this program with every other program of libs/xactor/example/, and compares what each prints with the output this page shows (Testing). A b2 project uses xactor through /webcpp/xactor//xactor, which adds xactor’s headers and Boost’s to whatever names it.

The model

A program writes two things, its messages and the logic of its actors, and xactor provides the rest:

Messages

One closed std::variant of plain structs, the program’s own. Closed means every message is known when the program is compiled: std::visit over it checks that every alternative is handled, and the scheduler’s log records which alternative arrived by its index(). Every type of xactor that carries a message is a template over this variant, and xactor asks nothing more of it than to be a std::variant that can be moved. A program may name the types it uses once, for instance using turn = xactor::turn<message>;.

Actor logic

What an actor does: a class with one handle, called with one message at a time (Writing an actor logic). It never sees the scheduler, its mailbox or the driver: all it does, it does through its turn, the object it is handed with each message (Turns).

Actor

A logic running in a scheduler, created by create_actor or by a turn’s spawn_child, and named by the actor_ref they return. An actor_ref holds the actor’s address, a number from 1 in the order the actors were created; 0 names no actor.

Scheduler

The system that holds the actors: one first-in first-out mailbox for each actor, and one first-in first-out queue of the actors that have messages waiting (The scheduler). Delivering a message only puts it in a mailbox.

Driver

What decides when turns happen: it hands queued messages to their actors until none is left, or until it has handed a given number (Drivers).

Execution

Everything that one delivery from the host causes, named by a correlation_id. Its fuel pays for all of it (Fuel).

A message travels in an envelope, xactor::envelope<message>, which is what a logic’s handle receives:

Field What it holds

to

The actor it is for.

from

The actor that sent it; the address 0 when the host did, and the actor itself for the message of one of its timers.

correlation

The execution it belongs to.

sequence

Its number in the order messages entered a mailbox: the scheduler numbers each one as it enqueues it.

payload

The message.

A run goes the same way every time. The host creates the actors and delivers a message, which waits in a mailbox. A driver then calls the scheduler’s run_one, which takes the actor at the front of the queue and hands it the first message of its mailbox: the actor’s handle runs with that message and its turn, and whatever the actor sends waits in another mailbox. When nothing is left, the host reads what the actors ended with.

Writing an actor logic

An actor logic is a class derived from xactor::actor_logic<Message> that overrides its one pure virtual function, handle, which takes the actor’s turn and the envelope of the message, and returns an xactor::result<void>, as the quick start’s echo and counter do (Quick start).

  • Its input. What the logic is constructed with is its input: create_actor<Logic>(system, inputs) and a turn’s spawn_child<Logic>(inputs) pass their arguments to the constructor of Logic. The quick start’s counter takes the echo’s actor_ref as its input.

  • Its state. Its members are the actor’s state, and only its own handle changes them: the counter’s sum and count of answers.

  • One message at a time. handle is called once for each message, and returns before the next is handed over. It reads the message from cause.payload, with std::get_if, std::holds_alternative or std::visit.

  • Owned by the scheduler. A logic can be neither copied nor moved: once it runs, the scheduler owns it and never moves it.

  • Errors end it. A handle that returns an error ends its actor with the status error, and the scheduler keeps the error for error_of. The rest of the system goes on: every other actor keeps its turns, and the driver’s run goes on. A handler may return any boost::system::error_code, of xactor’s category or of its own; the scheduler keeps it as it is.

Every call of xactor that can fail returns an xactor::result<T>, Boost.System’s result with an error_code, and changes nothing when it fails. A logic checks the result of each call: it returns a failure to end its actor with it, as the quick start’s counter does with a failed send, or it goes on, as the relay of Fuel does. xactor::failure<T>(code) builds a failed result<T>. xactor’s own codes are xactor::errc values, of the category webcpp.xactor, and their message() is their name:

Code When

errc::invalid_argument

A null logic given to spawn; an address that names no actor, given to deliver, send, stop or status_of; a reply to a message from the host; a turn’s finish or stop once its actor has ended, a second finish among them; a stop_child of an actor that is not the caller’s child; a wake_at or a spawn_child once the actor has ended.

errc::resource_limit

A send, reply, wake_at or spend that the execution’s fuel cannot pay (Fuel).

The scheduler

xactor::scheduler<Message> holds the actors and hands them their messages. It is built with its budgets, whose fuel is the fuel of each execution, a million units by default. Each actor has a first-in first-out mailbox, and the actors with messages waiting wait in one first-in first-out queue, each at most once. run_one takes the actor at the front of the queue, hands it the first message of its mailbox, and puts it back at the end of the queue while its mailbox holds more. So no actor keeps the others waiting however much it sends itself, and the order of deliveries depends on nothing but the order in which messages were enqueued. Nothing in xactor starts a thread: the scheduler and its actors live on the thread that calls them, the host’s.

The quick start’s host shows the calls a host makes most (Quick start):

Call What it does

scheduler(budgets)

A system whose executions each start with budgets.fuel.

create_actor<Logic>(system, inputs)

A free function: constructs a Logic from inputs and runs it as a new actor of system, active, at the next address. Returns its actor_ref.

spawn(logic)

The same, for a logic already constructed, given as a std::unique_ptr to an actor_logic<Message>; invalid_argument for a null one.

deliver(to, message, correlation)

Puts a message from the host in the mailbox of to, in the execution correlation, and the actor in the queue; nothing runs yet. invalid_argument for an address that names no actor.

run_one(log)

Hands exactly one message to its actor, and records it in log when log is not null. Returns false when there was none. Drivers call it.

idle()

Whether no message is waiting.

status_of(actor)

active, done, error or stopped (Lifecycle); invalid_argument for an address that names no actor.

output_of(actor)

What the actor finished with, or nullptr unless it is done.

error_of(actor)

The error its handler returned, or an empty code unless it is error.

fuel_of(correlation), limits()

The fuel an execution has left, and the budgets the system was built with (Fuel).

clock_tick, release_next, now, pending_timers, next_deadline

Time and timers (Timers).

stop(actor)

Ends an actor and its family from outside (Lifecycle).

A failed handler does not fail run_one: the actor ends with the status error, and run_one returns true for the message it handed over. It returns a result all the same, so that a failure of the scheduler itself would have a way to the host, through the drivers, which pass it on; there is none today.

Turns

A turn is one delivery: the scheduler calls an actor’s handle with one message and an xactor::turn<Message>, and the turn is everything the actor may do to the system while it handles that message. The logic never sees the scheduler, so every effect goes through the turn, which knows the actor that is acting and the execution the message belongs to: a message the actor sends carries it as its sender, and is paid from the fuel of that execution.

The turn lives for one handle. It holds references to the scheduler and to the message, so a logic never keeps it, nor a reference into the message.

Call What it does Fuel

send(to, message)

Sends a message to an actor, itself included.

1

reply(message)

Sends a message to the sender of the message being handled, in its execution.

1

spawn_child<Logic>(inputs)

Constructs a Logic from inputs and runs it as a new actor of the same system, a child of this one. Returns its actor_ref (Lifecycle).

0

stop_child(child)

Stops a child of this actor and every actor below it. Returns the actors it stopped (Lifecycle).

0

finish(output)

Ends this actor with an output: it becomes done, and receives nothing more. Once only.

0

stop()

Ends this actor as stopped, alone: its children are its logic’s to stop. Once only.

0

wake_at(deadline, message)

Arms a timer: the message comes back to this actor when the host’s time reaches deadline (Timers).

1

wake_at(deadline, message, key)

The same, replacing this actor’s timer under key.

1

cancel_timer(key)

Removes this actor’s timer under key, if it has one.

0

now()

The time the host brought last (Timers).

0

spend(units)

Pays for work this actor does itself, all the units or none (Fuel).

units

fuel()

The fuel this execution has left.

0

self(), parent()

This actor, and the actor whose turn spawned it, none for an actor the host created.

0

The quick start’s logics use some of these calls (Quick start): the counter sends its asks with send and ends with finish, and the echo answers with reply, so it never needs to know who asked. The sections that follow show the others at work: spend and fuel in Fuel, wake_at, cancel_timer and now in Timers, and spawn_child in Lifecycle.

The rules of a turn:

  • A send is queued. A message sent waits in its target’s mailbox, and its target handles it in a turn of its own, later, in the order of the queue; nothing an actor sends runs inside its own handle.

  • A reply needs a sender. A message from the host has none, and a reply to it is invalid_argument.

  • Every call that can fail returns a result, and changes nothing when it fails. The logic returns the failure to end its actor with it, or goes on.

  • An address must name an actor. A send to an address that names no actor is invalid_argument, and nothing is paid. A send to an actor that has ended is paid, and the message is dropped, as every message to an actor that has ended is (Lifecycle).

  • The rest of the turn runs after finish or stop. The actor receives nothing more, and in the rest of that turn it can still send, but a wake_at or a spawn_child is invalid_argument, and nothing is paid or constructed.

Fuel

Every message belongs to an execution, named by a correlation_id. A delivery from the host opens its execution, and every message that delivery causes, directly or through any number of actors, belongs to the same one. An execution has one reservoir of fuel, the fuel of the scheduler’s budgets, and every actor in it pays from that reservoir: one unit for each message it sends and each timer it arms, and the units it declares for its own work.

Fuel exists so that the total work of an execution is bounded, whatever shape its messages take: a chain of messages, a fan-out, two actors that answer each other forever, or an actor that computes a lot and sends little. A host that runs actors it does not trust, or that must answer within a budget, knows that every delivery it makes ends.

The smallest example is an actor that sends itself a message on every turn, for as long as the execution can pay, and prints the fuel left at the start of each turn:

/** Sends itself a hop on every turn, for as long as the execution can pay. */
class relay final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& /*cause*/) override {
        std::cout << "relay: fuel " << turn.fuel() << '\n';
        const xactor::result<void> sent = turn.send(turn.self(), hop{});
        if (!sent.has_value()) {
            std::cout << "relay: refused, " << sent.error().message() << '\n';
        }
        return {};
    }
};

With a budget of 3, the host delivers start in execution 1:

xactor::scheduler<message> system(xactor::budgets{.fuel = 3});
const xactor::result<xactor::actor_ref> relaying = xactor::create_actor<relay>(system);
if (!relaying.has_value()) {
    return 1;
}
const xactor::correlation_id first{.value = 1};
if (!system.deliver(*relaying, start{}, first).has_value()) {
    return 1;
}
xactor::fifo_driver<message> driver(system);
const xactor::result<std::size_t> delivered = driver.run_until_idle(nullptr);
if (!delivered.has_value()) {
    return 1;
}
std::cout << "delivered " << *delivered << ", relay is "
          << spelling(system.status_of(*relaying).value()) << '\n';

spelling, a helper of the program, writes a status as its name. It prints:

relay: fuel 3
relay: fuel 2
relay: fuel 1
relay: fuel 0
relay: refused, resource_limit
delivered 4, relay is active

The host’s start costs nothing, so the relay’s first turn sees all 3 units. Each send takes one, so the fourth turn finds none: its send fails with resource_limit, nothing is sent, and the run ends after four deliveries. A refusal is a value the logic decides about. The relay prints it and returns success, so it stays active, ready for the next execution.

A second delivery with another correlation opens a second execution, with fuel of its own:

const xactor::correlation_id second{.value = 2};
if (!system.deliver(*relaying, start{}, second).has_value()) {
    return 1;
}
const xactor::result<std::size_t> again = driver.run_until_idle(nullptr);
if (!again.has_value()) {
    return 1;
}
std::cout << "execution 1 has " << system.fuel_of(first) << " left, execution 2 has "
          << system.fuel_of(second) << " left\n";

It prints:

relay: fuel 3
relay: fuel 2
relay: fuel 1
relay: fuel 0
relay: refused, resource_limit
execution 1 has 0 left, execution 2 has 0 left

Execution 2 runs the same four turns on its own 3 units, and execution 1 still has none: fuel is never refilled. Two deliveries with the same correlation share one execution, and a delivery to an execution that has spent all its fuel delivers its message, whose sends are then refused. A host gives each input a correlation of its own.

An actor that computes without sending declares what its work costs with spend, so that it is bounded too. This worker pays 2 units on every turn, then sends itself a message, and returns a refusal instead of ignoring it:

/** Pays 2 units for its own work on every turn, then sends itself a hop. */
class worker final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& /*cause*/) override {
        const xactor::result<void> paid = turn.spend(2);
        if (!paid.has_value()) {
            return paid;
        }
        return turn.send(turn.self(), hop{});
    }
};
const xactor::result<xactor::actor_ref> working = xactor::create_actor<worker>(system);
if (!working.has_value()) {
    return 1;
}
const xactor::correlation_id third{.value = 3};
if (!system.deliver(*working, start{}, third).has_value()) {
    return 1;
}
const xactor::result<std::size_t> worked = driver.run_until_idle(nullptr);
if (!worked.has_value()) {
    return 1;
}
std::cout << "worker is " << spelling(system.status_of(*working).value()) << " ("
          << system.error_of(*working).message() << "), execution 3 has "
          << system.fuel_of(third) << " left\n";

It prints:

worker is error (resource_limit), execution 3 has 0 left

The first turn spends 2 of the 3 units and sends with the last one. The second turn’s spend(2) finds none, and fails with resource_limit; the worker returns the failure, which ends it with the status error, and error_of keeps the code.

The rules of fuel:

  • What costs. Inside a turn, send and reply cost one unit, wake_at, with a key or without, one unit, and spend(n) n units, all from the execution of the message being handled. spawn_child, stop_child, stop, finish, cancel_timer, self, parent, now and fuel cost nothing, and so does a delivery from the host.

  • All or nothing. What the execution cannot pay fails with resource_limit and changes nothing: no message is enqueued, no timer armed, no unit spent. spend takes all its units or none, and a keyed wake_at that is refused leaves the timer under its key as it was.

  • Opening an execution. The first deliver, clock_tick or release_next of a correlation opens its execution with the whole budget. A timer that a tick releases belongs to the tick’s execution, and releasing it costs nothing, since arming it was paid (Timers).

  • Reading it. turn.fuel() is the fuel the execution of the message being handled has left, and the scheduler’s fuel_of(correlation) the same from outside; for an execution not yet opened, fuel_of is the whole budget.

Timers

An actor that must act later arms a timer: turn.wake_at(deadline, message) keeps the message until the host’s time reaches deadline, then puts it in the actor’s own mailbox. xactor never reads a clock. Time is a number the host brings, with the scheduler’s clock_tick or release_next, so a run with timers is as repeatable as one without, and a test moves time by any amount at once. The unit is the host’s to choose; the example counts milliseconds.

A kettle boils 100 ms after start and whistles 120 ms after it. Its boil is a keyed timer, under the key "boil": delay arms the boil again under the same key, which replaces the one start armed, and cancel removes it. A deadline is a time, so the kettle computes each from the time the host brought last, turn.now():

/** Boils 100 ms after start and whistles 120 ms after it; delay and cancel move the boil. */
class kettle final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        if (std::holds_alternative<start>(cause.payload)) {
            const xactor::result<void> armed = turn.wake_at(turn.now() + 100, boiled{}, "boil");
            if (!armed.has_value()) {
                return armed;
            }
            return turn.wake_at(turn.now() + 120, whistle{});
        }
        if (std::holds_alternative<delay>(cause.payload)) {
            // The same key: this timer replaces the one start armed.
            return turn.wake_at(turn.now() + 150, boiled{}, "boil");
        }
        if (std::holds_alternative<cancel>(cause.payload)) {
            turn.cancel_timer("boil");
            return {};
        }
        if (std::holds_alternative<boiled>(cause.payload)) {
            std::cout << "boiled at " << turn.now() << '\n';
            return {};
        }
        std::cout << "whistle at " << turn.now() << '\n';
        return {};
    }
};

The host delivers start and then delay, each in an execution of its own, at the time 0, before any tick. deliver, a helper of the program, delivers one message from the host in the execution it is given and runs the system until no message is left; print_timers writes how many timers are armed and the earliest deadline, from the scheduler’s pending_timers() and next_deadline():

if (!deliver(system, *boiling, start{}, 1)) {
    return 1;
}
print_timers(system);
if (!deliver(system, *boiling, delay{}, 2)) {
    return 1;
}
print_timers(system);

It prints:

timers 2, next deadline 100
timers 2, next deadline 120

start armed two timers, the boil at 100 and the whistle at 120. delay replaced the boil with one at 150, so two timers are still armed, and the earliest is now the whistle’s.

The host then brings the time, with clock_tick and with release_next:

if (!system.clock_tick(130, xactor::correlation_id{.value = 3}).has_value()) {
    return 1;
}
if (!driver.run_until_idle(nullptr).has_value()) {
    return 1;
}
print_timers(system);
const xactor::result<bool> released =
    system.release_next(200, xactor::correlation_id{.value = 4});
if (!released.has_value() || !*released) {
    return 1;
}
if (!driver.run_until_idle(nullptr).has_value()) {
    return 1;
}
print_timers(system);

It prints:

whistle at 130
timers 1, next deadline 150
boiled at 200
timers 0, next deadline none

clock_tick(130) moves the time to 130 and releases every timer due by then, the whistle alone, into execution 3; the driver delivers it, and the kettle whistles at 130, the time of the tick, which is past the whistle’s deadline. The boil, due at 150, waits. release_next(200) moves the time to 200 and releases only the first timer due by then, and returns whether there was one. With one timer due, both calls do the same; with several, a host that runs the system between two calls of release_next lets the handling of one timer cancel or replace a later one of the same tick, where clock_tick has released them all before any is handled.

Last, at the time 200, start arms a boil at 300 and a whistle at 320, and cancel removes the boil before it is due:

if (!deliver(system, *boiling, start{}, 5) || !deliver(system, *boiling, cancel{}, 6)) {
    return 1;
}
print_timers(system);
if (!system.clock_tick(400, xactor::correlation_id{.value = 7}).has_value()) {
    return 1;
}
if (!driver.run_until_idle(nullptr).has_value()) {
    return 1;
}
print_timers(system);

It prints:

timers 1, next deadline 320
whistle at 400
timers 0, next deadline none

The rules of timers:

  • Order. Due timers are released in the order of their deadlines, and timers with the same deadline in the order they were armed. A timer that replaces another is armed anew, and takes its place in that order from then.

  • Only the host moves time. No timer fires without a clock_tick or a release_next that reaches its deadline. now(), on the scheduler or the turn, is the time the last of them brought, 0 before any, so a delay becomes a deadline as turn.now() + delay.

  • Released into the tick’s execution. A released timer’s message belongs to the execution the host gave clock_tick or release_next, and releasing it costs nothing, since arming it was paid. Its envelope’s from is the actor itself.

  • Keys belong to their actor. wake_at with a key replaces this actor’s timer under that key, and cancel_timer(key) removes it, for free, and does nothing when there is none. Two actors may use the same key. A keyed wake_at that the execution cannot pay leaves the timer under its key as it was.

  • An ended actor has no timers. An actor that ends, done, error or stopped, loses its timers, and in the rest of the turn that ended it, its wake_at is invalid_argument, and nothing is paid.

  • Time can move back. A tick earlier than the last moves now back: it releases what is due by then, and a deadline computed afterwards from now is earlier than one computed before it. A host that wants time to move forward only never brings an earlier time.

Lifecycle

Every actor has a status, which status_of returns. It starts active, the one status in which it receives messages, and it ends in one of the others, each final:

Status Value How an actor gets it What it keeps

active

1

Every actor starts here: create_actor, spawn and a turn’s spawn_child create it active.

Its mailbox and its timers.

done

2

Its turn called finish(output).

The output, for output_of.

error

3

Its handle returned an error.

The error, for error_of.

stopped

4

The host’s stop, its parent’s stop_child, or either of them for one of its ancestors, stopped it; or its own turn called stop().

Nothing.

The values are fixed and never reused, because a system that persists its actors rebuilds them from these numbers.

Actors form families. An actor spawned in a turn is a child of that turn’s actor: it records the actor as its parent, which turn.parent() returns, and the parent records its children in the order it spawned them. An actor the host created has no parent. A family is stopped together, from its parent or by the host, each actor after its own descendants, siblings in the order they were spawned.

In the example, a supervisor spawns three children, each of which ends differently or not at all:

/** Finishes with twice the value of its first work: it is then done, with an output. */
class doubler final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        if (const auto* asked = std::get_if<work>(&cause.payload)) {
            return turn.finish(answer{.value = asked->value * 2});
        }
        return {};
    }
};

/** Fails on its first work: it returns an error, and ends with the status error. */
class failing final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle(xactor::turn<message>& /*turn*/,
                                const xactor::envelope<message>& cause) override {
        if (std::holds_alternative<work>(cause.payload)) {
            return xactor::failure<void>(xactor::errc::invalid_argument);
        }
        return {};
    }
};

/** Waits for work and never ends by itself. */
class idle final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle(xactor::turn<message>& /*turn*/,
                                const xactor::envelope<message>& /*cause*/) override {
        return {};
    }
};

On start, the supervisor spawns them, in this order, and gives each work:

/** On start, spawns a doubler, a failing child and an idle one, and gives each work. */
class supervisor final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        if (!std::holds_alternative<start>(cause.payload)) {
            return {};
        }
        const xactor::result<xactor::actor_ref> first = turn.spawn_child<doubler>();
        if (!first.has_value()) {
            return {boost::system::in_place_error, first.error()};
        }
        const xactor::result<xactor::actor_ref> second = turn.spawn_child<failing>();
        if (!second.has_value()) {
            return {boost::system::in_place_error, second.error()};
        }
        const xactor::result<xactor::actor_ref> third = turn.spawn_child<idle>();
        if (!third.has_value()) {
            return {boost::system::in_place_error, third.error()};
        }
        for (const xactor::actor_ref child : {*first, *second, *third}) {
            const xactor::result<void> sent = turn.send(child, work{.value = 21});
            if (!sent.has_value()) {
                return sent;
            }
        }
        return {};
    }
};

The host creates the supervisor, delivers start, and runs the system until no message is left. print_statuses, a helper of the program, writes the status of the actors at the addresses 1 to 4, and spelling a status as its name:

xactor::scheduler<message> system(xactor::budgets{.fuel = 100});
const xactor::result<xactor::actor_ref> root = xactor::create_actor<supervisor>(system);
if (!root.has_value()) {
    return 1;
}
if (!system.deliver(*root, start{}, xactor::correlation_id{.value = 1}).has_value()) {
    return 1;
}
xactor::fifo_driver<message> driver(system);
if (!driver.run_until_idle(nullptr).has_value()) {
    return 1;
}
print_statuses(system, 4);
const message* output = system.output_of(xactor::actor_ref{.value = 2});
if (output == nullptr) {
    return 1;
}
std::cout << "actor 2 finished with " << std::get<answer>(*output).value << '\n';
std::cout << "actor 3 failed with " << system.error_of(xactor::actor_ref{.value = 3}).message()
          << '\n';

It prints:

actor 1: active
actor 2: done
actor 3: error
actor 4: active
actor 2 finished with 42
actor 3 failed with invalid_argument

The supervisor is actor 1, and its children took the next addresses in the order it spawned them: the doubler 2, the failing child 3 and the idle one 4. The doubler finished with 42 and is done; the failing child returned invalid_argument and ended with the status error, while the rest of the system went on; the idle child and the supervisor are still active. Neither of the children that ended told the supervisor: xactor does not tell a parent that its child has ended. A child that must tell its parent sends it a message, to turn.parent(), in the turn it ends.

Then the host stops the supervisor, and delivers it another start:

const xactor::result<std::vector<xactor::actor_ref>> stopped = system.stop(*root);
if (!stopped.has_value()) {
    return 1;
}
std::cout << "stopped:";
for (const xactor::actor_ref actor : *stopped) {
    std::cout << ' ' << actor.value;
}
std::cout << '\n';
print_statuses(system, 4);
if (!system.deliver(*root, start{}, xactor::correlation_id{.value = 2}).has_value()) {
    return 1;
}
const xactor::result<std::size_t> delivered = driver.run_until_idle(nullptr);
if (!delivered.has_value()) {
    return 1;
}
std::cout << "delivered to a stopped actor: " << *delivered << '\n';

It prints:

stopped: 4 1
actor 1: stopped
actor 2: done
actor 3: error
actor 4: stopped
delivered to a stopped actor: 0

stop visits the family in its order, the children 2, 3 and 4 and then their parent 1, and stops each that is still active: the doubler and the failing child stay as they ended, so it stops 4 and then 1, and returns them in that order. The start delivered afterwards is accepted, since the address names an actor, and dropped, since the actor has ended: nothing is delivered.

The rules of the lifecycle:

  • Only an active actor receives messages. Once an actor has ended, its pending messages are dropped, never delivered and never logged, and so is every later message to it. Dropping is not an error of the sender’s, which cannot know the actor has ended.

  • An actor finishes once. finish(output) makes it done and keeps the output; a second finish is invalid_argument and changes nothing. output_of is nullptr unless the actor is done, and error_of is empty unless it is error.

  • An actor ends in one way. An error that handle returns in the turn that finished or stopped its actor changes nothing: the actor stays as it ended.

  • Stopping a child stops its family. turn.stop_child(child) stops child and every actor descended from it that is still active, in the family’s order, and returns them in that order; their mailboxes and timers are dropped. A member that had already ended stays as it ended, and its descendants are stopped all the same. It is invalid_argument for anything but a child of the caller: the caller itself, a grandchild, an address that names no actor. The host’s stop(actor) does the same for any actor.

  • Ending alone. turn.stop() ends the actor handling the message as stopped, alone. An actor that finishes, fails or stops itself leaves its children running: they are its logic’s to stop, with stop_child, before it ends.

  • An ended actor creates nothing. In the rest of the turn that ended it, an actor spawns no child and arms no timer: spawn_child and wake_at are invalid_argument, and spawn_child constructs nothing. It can still send.

Drivers

The scheduler decides what a turn is, and a driver decides when turns happen: it calls the scheduler’s run_one until no message is left, or until it has delivered as many as its host allows. So the same actors run under a plain loop in a program, one message at a time in a test, or as handlers on a Boost.Asio strand, and every driver delivers the same messages in the same order. xactor’s drivers are these:

Driver For What it does

fifo_driver

A program, native or WASI

run_until_idle(log) delivers until no message is left, and returns how many it delivered. run_turn(n, log) delivers at most n, for a host that takes the thread back between turns.

test_driver

A test

step() and step(log) deliver exactly one message, and return whether there was one. It has nothing else.

asio_driver

A native program that runs its turns as Asio handlers

run_until_idle(log), the same turns posted onto one strand of an io_context the driver owns. A WASI build leaves it out.

A driver is built over one scheduler, which it holds by reference. The log of run_until_idle and run_turn is an xactor::envelope_log*, and nullptr records nothing; test_driver takes an envelope_log& in step(log), and records nothing in step().

The example plays a rally between two players. A serve from the host tells a player whom to serve to, and each player returns the ball until the rally reaches 4:

/** Returns the ball until the rally reaches 4. */
class player final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        if (const auto* served = std::get_if<serve>(&cause.payload)) {
            return turn.send(served->to, ball{.rally = 1});
        }
        const int rally = std::get<ball>(cause.payload).rally;
        if (rally == 4) {
            return {};
        }
        return turn.reply(ball{.rally = rally + 1});
    }
};

set_up, a helper of the program, creates two players, at the addresses 1 and 2, and delivers the serve from the host to the first; print writes each entry of a log. The program sets up the same rally twice, on two schedulers. The first runs with a test_driver, one step at a time, recording each delivery:

xactor::scheduler<message> stepped(xactor::budgets{.fuel = 100});
if (set_up(stepped).value == 0) {
    return 1;
}
xactor::test_driver<message> one_at_a_time(stepped);
xactor::envelope_log stepped_log;
std::size_t steps = 0;
while (true) {
    const xactor::result<bool> ran = one_at_a_time.step(stepped_log);
    if (!ran.has_value()) {
        return 1;
    }
    if (!*ran) {
        break;
    }
    ++steps;
}
std::cout << "test_driver: " << steps << " steps\n";
print(stepped_log);

It prints:

test_driver: 5 steps
  #1 to 1 from 0, fuel 100
  #2 to 2 from 1, fuel 99
  #3 to 1 from 2, fuel 98
  #4 to 2 from 1, fuel 97
  #5 to 1 from 2, fuel 96

Five messages were delivered: the serve, from the host, whose address is 0, and four balls. Each entry shows the execution’s fuel when its message was delivered: the serve cost nothing, and each ball one unit.

test_driver is how a test drives actors: each step delivers one message, so after each one the test sees what that message changed, through status_of, output_of, error_of and fuel_of. A test that constructs a logic itself and hands it to the scheduler’s spawn can keep a pointer to it and read its members too, since the scheduler owns the logic and never moves it. And a log recorded with step(log) is compared with the log the test expects, or with the log of another run of the same input, which must be equal (Testing a program’s actors).

The second runs with a fifo_driver, first for at most two messages, then until none is left, and compares the two logs:

xactor::scheduler<message> turned(xactor::budgets{.fuel = 100});
if (set_up(turned).value == 0) {
    return 1;
}
xactor::fifo_driver<message> fifo(turned);
xactor::envelope_log turned_log;
const xactor::result<std::size_t> first = fifo.run_turn(2, &turned_log);
if (!first.has_value()) {
    return 1;
}
std::cout << "fifo_driver: run_turn(2) delivered " << *first << ", idle " << std::boolalpha
          << turned.idle() << '\n';
const xactor::result<std::size_t> rest = fifo.run_until_idle(&turned_log);
if (!rest.has_value()) {
    return 1;
}
std::cout << "fifo_driver: run_until_idle delivered " << *rest << ", idle " << turned.idle()
          << '\n';
std::cout << "the two logs are equal: " << (stepped_log == turned_log) << '\n';

It prints:

fifo_driver: run_turn(2) delivered 2, idle false
fifo_driver: run_until_idle delivered 3, idle true
the two logs are equal: true

run_turn(2, log) returned after two messages with work still waiting, and run_until_idle delivered the other three. However the run was cut, the deliveries were the same, and so are the logs.

The envelope log

The envelope log is the evidence that two runs, or two drivers, delivered the same messages the same way. run_one records an entry for every message it delivers, before the actor handles it, and nothing for a message that is dropped:

Member of logged_envelope What it holds

sequence

The message’s sequence number.

to, from

Its receiver, and its sender, 0 for the host.

correlation

Its execution.

remaining_fuel

The fuel its execution had when it was delivered.

payload_index

Which alternative of the message variant it was, its index().

The log keeps which alternative arrived, not the message, so every entry has the same size and two logs compare cheaply, with ==. entries() returns them in order. Two schedulers built with the same budgets and fed the same deliveries record equal logs, whatever driver runs them.

The Asio driver

asio_driver delivers the same turns on Boost.Asio, natively: the turns run as handlers on a strand of an io_context the driver owns, on the thread that calls run_until_idle. The context is the driver’s alone: it offers no executor, so the program’s own I/O objects run on a context of their own, never on the driver’s. The driver posts each run_one onto the strand and drains the context with poll, which runs what is ready and returns. It never calls the context’s run, run_one or run_for, which would wait for more work, so a turn never blocks.

This program runs the rally of the example above with a fifo_driver, and again with an asio_driver; it repeats the players and set_up of that example, as a program of its own:

xactor::scheduler<message> posted(xactor::budgets{.fuel = 100});
if (set_up(posted).value == 0) {
    return 1;
}
xactor::asio_driver<message> asio(posted);
xactor::envelope_log asio_log;
const xactor::result<std::size_t> by_asio = asio.run_until_idle(&asio_log);
if (!by_asio.has_value()) {
    return 1;
}
std::cout << "fifo_driver delivered " << *by_fifo << ", asio_driver delivered " << *by_asio
          << '\n';
std::cout << "the two logs are equal: " << std::boolalpha << (fifo_log == asio_log) << '\n';

It prints:

fifo_driver delivered 5, asio_driver delivered 5
the two logs are equal: true

This is the one program of the page built natively only: Boost.Asio 1.92 does not compile for wasm32-wasip2 or wasm32-wasip3 with wasi-sdk 34, so drivers.hpp leaves asio_driver out of a WASI build, and Boost.Asio’s headers with it.

Static actors

A static actor is an actor whose behaviour is a state machine compiled into the program: its states, its events and its transitions are C++ types and expressions, checked by the compiler and fixed when the program is built. It suits behaviour known when the program is written. The compiler checks its table, and nothing is read or interpreted at run time; in exchange, a change to the machine is a change to the program, and the machine cannot come from a file or a request.

xactor holds no adapter for a state machine library, and needs none. An actor logic is any class with a handle, so a logic holds a machine as a member and hands each message to it, and the machine’s actions reach the system through the turn. The program joins them with an event type that carries the turn and a handle of a few lines, and nothing else. Nothing in xactor knows the library, and the library does not know xactor, so any library whose machines take typed events joins the same way. This section writes one machine with Boost.MSM, one of Boost’s libraries.

The machine does not receive the message alone. It receives an on<Event>, which holds the message and the turn it arrived in, both by reference:

/** What the machine receives for a message: the message, and the turn it arrived in. */
template <class Event>
struct on {
    const Event& event;
    xactor::turn<message>& turn;
};

An action takes the turn from its event, so it can reply, send, finish or arm a timer as any logic does, paid from the right execution. The on<Event> is built inside handle and lives for one call of the machine, so its references never outlive the turn.

The machine is a turnstile, the first machine of many state machine tutorials. It starts locked. A coin unlocks it, and a push goes through and locks it again. A push while it is locked sounds an alarm, and a coin while it is unlocked is refunded: the turnstile replies alarm or refund to the actor that pushed or paid. A visitor puts a coin in, pushes twice and puts two coins in, all in one turn, and prints what comes back:

/** Puts a coin in, pushes twice, and puts two coins in; prints what comes back. */
class visitor final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        if (const auto* visiting = std::get_if<visit>(&cause.payload)) {
            const std::array<message, 5> plan{coin{}, push{}, push{}, coin{}, coin{}};
            for (const message& sent : plan) {
                const xactor::result<void> delivered = turn.send(visiting->turnstile, sent);
                if (!delivered.has_value()) {
                    return delivered;
                }
            }
            return {};
        }
        if (std::holds_alternative<alarm>(cause.payload)) {
            std::cout << "visitor: alarm\n";
        }
        if (std::holds_alternative<refund>(cause.payload)) {
            std::cout << "visitor: refund\n";
        }
        return {};
    }
};

The host creates the turnstile and a visitor, and delivers the visitor the address of the turnstile to visit:

xactor::scheduler<message> system(xactor::budgets{.fuel = 100});
const xactor::result<xactor::actor_ref> gate = xactor::create_actor<turnstile>(system);
const xactor::result<xactor::actor_ref> guest = xactor::create_actor<visitor>(system);
if (!gate.has_value() || !guest.has_value()) {
    return 1;
}
if (!system.deliver(*guest, visit{.turnstile = *gate}, xactor::correlation_id{.value = 1})
         .has_value()) {
    return 1;
}
xactor::fifo_driver<message> driver(system);
if (!driver.run_until_idle(nullptr).has_value()) {
    return 1;
}

It prints:

turnstile: locked
turnstile: unlocked
turnstile: locked
turnstile: unlocked
visitor: alarm
visitor: refund

The turnstile prints locked as soon as create_actor constructs it, since its machine enters its initial state then. The first coin, the first push and the second coin take it to unlocked, back to locked and to unlocked again. The second push and the last coin change no state, and enter none: they are answered with alarm and refund. The visitor hears the alarm after the turnstile’s last unlocked, because the alarm joined the queue behind the turnstile, which still had messages waiting, and the scheduler serves the actors with messages in turn (The scheduler).

The machine, with Boost.MSM

The program uses MSM’s classic back-end, msm::back::state_machine, and its functor front-end, whose rows are types in a Boost.MPL vector:

#include <webcpp/xactor.hpp>

#include <boost/mpl/vector.hpp>
#include <boost/msm/back/state_machine.hpp>
#include <boost/msm/front/functor_row.hpp>
#include <boost/msm/front/state_machine_def.hpp>

#include <array>
#include <iostream>
#include <type_traits>
#include <variant>

namespace xactor = webcpp::xactor;
namespace msm = boost::msm;
namespace mpl = boost::mpl;

MSM writes a machine as a front-end, a struct derived from msm::front::state_machine_def of itself, which holds the states, the table and the machine’s data:

/** Replies alarm to a push the turnstile does not let through. */
struct sound_alarm {
    template <class Machine, class Source, class Target>
    void operator()(const on<push>& received, Machine& machine, Source& /*source*/,
                    Target& /*target*/) {
        machine.outcome = received.turn.reply(alarm{});
    }
};

/** Replies refund to a coin the turnstile does not need. */
struct give_refund {
    template <class Machine, class Source, class Target>
    void operator()(const on<coin>& received, Machine& machine, Source& /*source*/,
                    Target& /*target*/) {
        machine.outcome = received.turn.reply(refund{});
    }
};

/** A turnstile: a coin unlocks it, a push goes through and locks it again. */
struct turnstile_chart : msm::front::state_machine_def<turnstile_chart> {
    struct locked : msm::front::state<> {
        template <class Event, class Machine>
        void on_entry(const Event& /*event*/, Machine& /*machine*/) {
            std::cout << "turnstile: locked\n";
        }
    };

    struct unlocked : msm::front::state<> {
        template <class Event, class Machine>
        void on_entry(const Event& /*event*/, Machine& /*machine*/) {
            std::cout << "turnstile: unlocked\n";
        }
    };

    using initial_state = locked;
    using none = msm::front::none;

    // clang-format off
    struct transition_table : mpl::vector<
        msm::front::Row<locked,   on<coin>, unlocked, none,        none>,
        msm::front::Row<locked,   on<push>, none,     sound_alarm, none>,
        msm::front::Row<unlocked, on<push>, locked,   none,        none>,
        msm::front::Row<unlocked, on<coin>, none,     give_refund, none>
    > {};
    // clang-format on

    /** A message the turnstile has no transition for is ignored. */
    template <class Machine, class Event>
    void no_transition(const Event& /*event*/, Machine& /*machine*/, int /*state*/) {}

    xactor::result<void> outcome;
};

Each state is a struct derived from msm::front::state<>, here with an on_entry, and initial_state names the first. Each row of transition_table is a Row<Source, Event, Target, Action, Guard>. none as its target makes an internal transition, which runs its action and leaves the state as it is, entering nothing: a push while the turnstile is locked sounds the alarm, and it stays locked. none as its action or its guard means there is none. An action is a functor, called with the event, the machine, the source state and the target state. The machine is the back-end, which derives from the front-end, so machine.outcome is the chart’s own member, into which sound_alarm and give_refund write the result of their reply. The table is kept one row per line, between clang-format off and on.

For an event the current state has no row for, MSM calls the front-end’s no_transition, and the default one asserts, which would stop the program, where assertions are on, on a message the actor does not take. The chart defines one that ignores the message.

The logic holds the back-end, an msm::back::state_machine of the turnstile_chart, and hands it each message:

/** A static actor: its behaviour is the turnstile's MSM machine, fixed when it is compiled. */
class turnstile final : public xactor::actor_logic<message> {
public:
    turnstile() { machine_.start(); }

    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        machine_.outcome = {};
        std::visit(
            [&](const auto& received) {
                machine_.process_event(
                    on<std::decay_t<decltype(received)>>{.event = received, .turn = turn});
            },
            cause.payload);
        return machine_.outcome;
    }

private:
    msm::back::state_machine<turnstile_chart> machine_;
};

An MSM machine enters its initial state when start() is called, so the logic’s constructor calls it, and the turnstile prints locked when create_actor constructs it. handle clears the outcome, visits the message, wraps the alternative it holds in an on<> with the turn, hands it to process_event, and returns the outcome: a reply that fails ends the actor with its error, as a failed send ends any logic that returns it.

Getting Boost.MSM

Boost.MSM is used through its headers alone, which a Boost installation has beside Boost’s other headers, so the program needs nothing more than xactor does. Neither xactor nor a program needs b2’s target for the library, /boost/msm//boost_msm, and neither uses it: it brings every library MSM depends on, Boost.Serialization among them, which is compiled.

The program is built and run natively, for wasm32-wasip2 and for wasm32-wasip3, with exceptions, and the machine needs none: a user’s build without them, exception-handling=off, runs it as well.

Rules that keep a run deterministic

xactor guarantees the order of a run. One first-in first-out mailbox for each actor and one first-in first-out queue of actors make the order of deliveries depend on the order of the messages alone, under every driver; fuel bounds every execution; and time moves only when the host brings it. xactor itself reads no clock, no disk and no network, blocks in no turn, keeps no static mutable state, so that two schedulers in one process are independent, and throws no exception of its own. Two schedulers built with the same budgets and fed the same deliveries record equal envelope logs (The envelope log).

An actor can still break that guarantee, by letting something other than its messages decide what it does. These rules keep it from doing so:

  • Read no clock in a turn. Take the time from turn.now(), or from the message a timer brings (Timers).

  • Read nothing from the world in a turn, and never wait. The host reads a file or the network, and delivers what it read as a message. A turn that waits holds up every actor, since they all run on one thread. Writing out, as the examples print, decides nothing, and is safe.

  • Draw no random number but from a seed that a message brought.

  • Keep no static mutable state. Two schedulers in one process must stay independent, as xactor’s own are.

  • Throw nothing, and return an error instead. xactor catches nothing: an exception thrown by a handle leaves run_one and the driver in the middle of the run, where an error returned ends one actor and the run goes on (Writing an actor logic).

  • Keep no reference to the turn or to a message beyond the turn. Both are gone once handle returns; copy what is needed.

A program checks that its run is deterministic by running the same deliveries twice, recording both runs and comparing the envelope logs, as xactor’s own tests compare its drivers (Testing).

Testing

A test of actors checks that, from a known start and given some messages, they reach the statuses and the outputs they should, and send what they should. A test is worth its checks only when the run it checks is the same every time, and in xactor nothing in a run depends on the world outside the test:

  • There is no real clock. A timer waits on the host’s time, and only the test moves it, by any amount at once (Timers).

  • There is no thread. The test’s driver hands over each message, and returns when it has (Drivers).

  • A failure is a value. A call of xactor that fails returns its error, and an actor whose handle fails ends with the status error, where the test reads the code (Writing an actor logic).

  • Work is counted. A test that gives an execution little fuel sees what its actors are refused when it runs out (Fuel).

This chapter shows how a program tests its actors, and then how xactor itself is tested, on every target it supports.

Testing a program’s actors

The test below checks an echo and the client that asks it, with Boost.Core’s lightweight_test, the framework of one header that xactor’s own tests use. It begins with xactor’s header and the framework’s:

#include <webcpp/xactor.hpp>

#include <boost/core/lightweight_test.hpp>

#include <cstdint>
#include <variant>
#include <vector>

namespace xactor = webcpp::xactor;

The echo replies to every ask with the same number, and the client asks it for the number its start names, and keeps each answer in a vector the test owns, which it is constructed with, so that the test reads what the client received:

/** Answers every ask with the same number. */
class echo final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        if (const auto* asked = std::get_if<ask>(&cause.payload)) {
            return turn.reply(answer{.value = asked->value});
        }
        return {};
    }
};

/** Asks the echo for the number a start names, and keeps each answer where the test reads it. */
class client final : public xactor::actor_logic<message> {
public:
    client(xactor::actor_ref echo, std::vector<int>& answers) : echo_(echo), answers_(answers) {}

    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        if (const auto* started = std::get_if<start>(&cause.payload)) {
            return turn.send(echo_, ask{.value = started->value});
        }
        if (const auto* answered = std::get_if<answer>(&cause.payload)) {
            answers_.push_back(answered->value);
        }
        return {};
    }

private:
    xactor::actor_ref echo_;
    std::vector<int>& answers_;
};

The first test case drives the actors with a test_driver, one message a step. After each step it checks what that message changed: the fuel of the execution, which each message an actor sends takes a unit of, and the answers the client kept. BOOST_TEST returns whether its check held, so the case stops where a check it cannot go on without fails:

/** Whether one step of the driver delivered a message. */
bool stepped(xactor::test_driver<message>& driver) {
    const xactor::result<bool> step = driver.step();
    return step.has_value() && *step;
}

/** One message a step: after each, the test checks what that message changed. */
void the_client_keeps_what_the_echo_answers() {
    std::vector<int> answers;
    xactor::scheduler<message> system(xactor::budgets{.fuel = 10});
    const xactor::result<xactor::actor_ref> echoing = xactor::create_actor<echo>(system);
    if (!BOOST_TEST(echoing.has_value())) {
        return;
    }
    const xactor::result<xactor::actor_ref> asking =
        xactor::create_actor<client>(system, *echoing, answers);
    if (!BOOST_TEST(asking.has_value())) {
        return;
    }
    const xactor::correlation_id execution{.value = 1};
    if (!BOOST_TEST(system.deliver(*asking, start{.value = 7}, execution).has_value())) {
        return;
    }

    xactor::test_driver<message> driver(system);
    BOOST_TEST(stepped(driver));  // The client handles start, and asks.
    BOOST_TEST_EQ(system.fuel_of(execution), 9U);
    BOOST_TEST(stepped(driver));  // The echo handles the ask, and answers.
    BOOST_TEST_EQ(system.fuel_of(execution), 8U);
    BOOST_TEST(answers.empty());
    BOOST_TEST(stepped(driver));  // The client handles the answer, and keeps it.
    BOOST_TEST(answers == std::vector<int>{7});
    const xactor::result<bool> last = driver.step();  // Nothing is left, and nothing failed.
    BOOST_TEST(last.has_value() && !*last);
    BOOST_TEST(system.idle());
}

The second runs the same deliveries twice, records each run in an envelope log, and checks that the two logs are equal, as every pair of runs of the same deliveries must be (The envelope log):

/** Runs the client and the echo on one start for each of `numbers`, and returns their log. */
xactor::envelope_log run(const std::vector<int>& numbers) {
    std::vector<int> answers;
    xactor::envelope_log log;
    xactor::scheduler<message> system(xactor::budgets{.fuel = 10});
    const xactor::result<xactor::actor_ref> echoing = xactor::create_actor<echo>(system);
    if (!BOOST_TEST(echoing.has_value())) {
        return log;
    }
    const xactor::result<xactor::actor_ref> asking =
        xactor::create_actor<client>(system, *echoing, answers);
    if (!BOOST_TEST(asking.has_value())) {
        return log;
    }
    std::uint64_t correlation = 0;
    for (const int number : numbers) {
        ++correlation;
        const xactor::correlation_id execution{.value = correlation};
        BOOST_TEST(system.deliver(*asking, start{.value = number}, execution).has_value());
    }
    xactor::fifo_driver<message> driver(system);
    BOOST_TEST(driver.run_until_idle(&log).has_value());
    return log;
}

/** Two runs of the same deliveries record equal envelope logs. */
void two_runs_record_the_same_log() {
    const xactor::envelope_log first = run({1, 2, 3});
    const xactor::envelope_log second = run({1, 2, 3});
    BOOST_TEST_EQ(first.entries().size(), 9U);
    BOOST_TEST(first == second);
}

main calls the cases and returns boost::report_errors(), the program’s status:

the_client_keeps_what_the_echo_answers();
two_runs_record_the_same_log();
return boost::report_errors();

When every check holds, the program prints No errors detected. on its standard error, nothing on its standard output, and returns 0. A check that fails prints its expression, its values, its file and its line on the standard error, and main returns 1. Built without exceptions, the program links a definition of boost::throw_exception, as any program does (Building it).

How xactor is tested

xactor’s tests are lightweight_test programs, one per subject, under test/, each declared in test/Jamfile with webcpp’s webcpp.run:

Test What it checks

scheduler

Delivery in first-in first-out order, within an actor and across actors; the test driver’s one message a step; the FIFO and the Asio drivers recording one envelope log; and timers: by deadline and then in the order armed, replaced and cancelled by key, refused, and dropped with their actor.

lifecycle

An actor’s statuses and how it ends: finishing, a failed handle, a stop. A family built with spawn_child, stopped descendants first by its parent or by the host, and a turn of an ended actor that spawns nothing.

fuel

Executions and fuel: a chain of messages that stops when its fuel runs out, a fan-out of messages that share it, an actor that pays for its own work and sees what is left, an execution of its own for each correlation, and a delivery from the host that costs nothing.

create_actor

create_actor, which constructs a new active actor that receives messages.

errors

The category webcpp.xactor: its name, the text of each errc and of a value no errc defines, the form of message that writes into a caller’s buffer and cuts the text to fit it, and the error_code an errc makes.

sequence_and_log

sequence_number::next, and the comparison of two envelope logs, equal and differing in each member of an entry, in length and in order.

config, config_developer_defined, config_all_off

Compile tests, which build config_test.cpp and run nothing: whether <webcpp/xactor/config.hpp> defines WEBCPP_XACTOR_NO_EXCEPTIONS exactly when webcpp has no exceptions, keeps the definition of a developer who defined it for xactor alone, and defines it when the developer defined WEBCPP_NO_EXCEPTIONS for every webcpp library.

Each test case is a function, and main calls them all and returns boost::report_errors(). A case that cannot go on after a failed check returns, if (!BOOST_TEST(...)) { return; }; a helper, which cannot return from its case, calls webcpp::test::require, of test/require.hpp, which ends the program with the errors counted so far. Each case is named after the rule it checks, as a_host_delivery_costs_no_fuel, and the comment above some of them cites a guarantee by its anchor (Guarantees).

Beside them, test/Jamfile compiles each public header alone, as the only include of its translation unit, so that a header that leans on an include it does not make fails, naming it; and example/Jamfile builds every program this page shows, runs it, and compares what it printed on its standard output with the <name>.expected beside it. A program that returns anything but 0 fails too, even when it printed the expected lines.

Targets

test/Jamfile and example/Jamfile declare the targets they are built for, webcpp.targets native wasip2 wasip3, and b2 builds each program for those alone:

Target How it is built and run

native

Any toolset not listed below. As the build makes it: with exceptions, unless its user asks for exception-handling=off, which links webcpp’s tools/throw_exception.cpp.

wasip2

The toolset clang-wasip2, the Clang of wasi-sdk for wasm32-wasip2, run by wasmtime. With exceptions, as WebAssembly’s exception handling, as on wasip3.

wasip3

The toolset clang-wasip3, for wasm32-wasip3, run by wasmtime. With exceptions, as WebAssembly’s exception handling.

That xactor works without exceptions is checked by the lint, which compiles every public header, and every test and example, which instantiate its templates, with exception-handling=off; a template that none of them instantiates is not checked.

<webcpp/xactor/config.hpp>, which every public header of xactor includes first, defines WEBCPP_XACTOR_NO_EXCEPTIONS when webcpp has no exceptions (WEBCPP_HAS_EXCEPTIONS is not defined: the compiler has none, or the program defined WEBCPP_NO_EXCEPTIONS), and a program may define it for xactor alone, to turn xactor’s exceptions off in a build that has them, as Boost.Asio’s BOOST_ASIO_NO_EXCEPTIONS does. xactor raises no exception of its own, so the macro changes nothing in it: it exists so that a program can set it for every webcpp library alike. The tests config, config_developer_defined and config_all_off check the three ways it is defined, and the lint refuses a throw in a public header, even in a template nothing instantiates: an exception goes through boost::throw_exception.

Boost.Asio does not compile for either WASI target, so the Asio driver, its checks in scheduler and the example xactor_asio are built natively only (The Asio driver). Every other program of test/ and example/ includes <webcpp/xactor.hpp>, and with it drivers.hpp, so its WASI build is the check that nothing of Boost.Asio enters a WASI build.

From the root of a webcpp checkout, these build and run the tests and the examples natively, then for each WASI target:

b2 libs/xactor/test libs/xactor/example
b2 libs/xactor/test libs/xactor/example toolset=clang-wasip2 testing.launcher=wasmtime
b2 libs/xactor/test libs/xactor/example toolset=clang-wasip3 testing.launcher=wasmtime

The WASI toolsets are b2’s clang, which a user-config.jam registers against wasi-sdk 34:

local wasi-sdk = /path/to/wasi-sdk ;

using clang : wasip2 : $(wasi-sdk)/bin/clang++
  : <cflags>--target=wasm32-wasip2 <cxxflags>--target=wasm32-wasip2
    <linkflags>--target=wasm32-wasip2
    <archiver>$(wasi-sdk)/bin/llvm-ar <ranlib>$(wasi-sdk)/bin/llvm-ranlib ;

using clang : wasip3 : $(wasi-sdk)/bin/clang++
  : <cflags>--target=wasm32-wasip3 <cxxflags>--target=wasm32-wasip3
    <linkflags>--target=wasm32-wasip3
    <archiver>$(wasi-sdk)/bin/llvm-ar <ranlib>$(wasi-sdk)/bin/llvm-ranlib ;

b2 compares timestamps, so a result that counts is built from scratch, with b2 -a. b2 libs/xactor/test//scheduler builds and runs one test alone, while working on it.

The test matrix

The continuous integration runs xactor’s tests and examples in lanes, one toolset each: the native compilers, and wasip2 and wasip3, the lanes that xactor’s Jamfiles declare. Each lane builds from scratch and records what b2 built and ran:

b2 -a --dump-tests --out-xml=<lane>.xml toolset=<toolset> libs/xactor/test libs/xactor/example

webcpp’s tools/report/report.py merges the files of every lane into a matrix in the manner of Boost’s regression tests: a page of the libraries by lanes, and a page of each library’s tests and examples by lanes. Each cell says whether that program passed in that lane, or how it failed: at compile time, at link time or when it ran. The output b2 captured of a failure is one click away. A lane in which nothing was built, or that was built for another target than its name says, fails by name rather than show green. The matrix is published with the documentation, at https://webcpporg.github.io/report/.

Guarantees

What xactor promises whatever its caller does: the rules its results keep, which the sections above teach and the tests check. A guarantee keeps its number, and the anchor named after it, so a comment of the code or of a test cites it by that anchor: (doc: #xactor-invariant-13) sends its reader to the thirteenth guarantee. b2 libs/xactor/doc, which builds this page, fails on a citation that names no anchor of it.

The scheduler, its turns, its drivers and its log:

  1. The messages of one mailbox are handled in the order they were enqueued.

  2. run_one takes the actor at the front of the queue and puts it back at the end while its mailbox is not empty, so delivery is first-in first-out across actors too.

  3. An actor is in the queue at most once.

  4. run_one handles exactly one message, and returns false only when the scheduler is idle. A handler that returns an error ends its actor with the status error and keeps the error for error_of, unless its turn had already ended the actor, which then stays as it ended; run_one still returns true, and every other actor keeps its turns.

  5. deliver, send, reply and a released timer put an active target actor in the queue when it is not there; a message to an actor that has ended is dropped (guarantee 23).

  6. Each correlation_id names one execution. The first deliver, clock_tick or release_next that names it opens it with budgets.fuel, and nothing refills it: a later delivery with the same correlation shares what it has left. A delivery from the host costs no fuel.

  7. Inside a turn, send and reply cost one unit, wake_at, with a key or without, one unit and spend(n) n units, all from the execution of the message being handled; spawn_child, stop_child, stop, finish, cancel_timer, self, parent, now and fuel cost nothing.

  8. What the execution cannot pay fails with resource_limit and changes nothing: nothing is enqueued, armed or spent, and a keyed wake_at leaves the timer under its key armed. spend takes all its units or none.

  9. turn::fuel is the fuel the execution of the message being handled has left; scheduler::fuel_of the same from outside, and the whole budget for an execution not yet opened.

  10. reply sends to the sender of the message being handled, in its execution. A message from the host has no sender, and a reply to it is invalid_argument, as a send to an address that names no actor is; neither is paid.

  11. Timers fire only for a deadline at or before the time the host brought, in (deadline, arming order) order. A timer replaced, cancelled or dropped never fires; a replacement takes a new arming order.

  12. No timer fires without a clock_tick or a release_next that reaches its deadline: time never advances by itself.

  13. A timer released by a clock_tick or a release_next belongs to that call’s execution, and its message’s sender is its own actor; releasing it costs nothing, since arming it was paid.

  14. run_until_idle returns only when idle() is true, and run_turn(n) handles at most n messages.

  15. fifo_driver and asio_driver produce identical envelope logs for the same deliveries.

  16. An envelope log given to run_one records {sequence, to, from, correlation, the fuel the execution had when the message was delivered, payload index} for every message delivered, before its actor handles it, and nothing for a dropped one.

  17. Two schedulers built with the same budgets and fed the same deliveries produce identical envelope logs.

  18. xactor reads no clock, no disk and no network.

  19. No turn blocks: nothing calls io_context::run, run_one or run_for.

  20. xactor throws no exception of its own: every operation that can fail returns a result, and the library compiles without exceptions, which the lint checks for every public header and for every test and example, which instantiate its templates. Where exceptions are on, an operation that allocates, such as spawn, deliver, send, wake_at or the log’s record, can still throw std::bad_alloc, as a standard container does.

  21. There is no static mutable state: two schedulers in one process are independent.

  22. test_driver exposes step, exactly one run_one, with or without a log, and nothing else; messages reach its scheduler through deliver.

  23. A spawned actor is active. Only an active actor receives messages: once it is done, error or stopped, its pending messages are dropped, and every later message to it is dropped too, never delivered and never logged. Dropping is not an error of the sender’s: a send to an actor that has ended succeeds, and is paid.

  24. finish(output) makes the actor handling the message done and keeps the output for output_of. An actor ends once: a finish once it has ended, a second finish or one after stop(), fails with invalid_argument and changes nothing.

  25. stop stops an actor and its family as stop_child does (guarantee 31) and returns the actors it stopped; an ended one stays as it ended.

  26. output_of is nullptr unless the status is done; error_of is empty unless the status is error.

  27. The values of status never change: active 1, done 2, error 3, stopped 4.

  28. The library names no message, id or error of any user. It asks of Message only that it be a std::variant that can be moved, whose index() the log records.

  29. create_actor<Logic>(system, inputs...) and a turn’s spawn_child<Logic>(inputs...) construct a Logic from inputs and run it as a new, active actor of the same system, returning its actor_ref; spawn does the same for a logic already constructed, and refuses a null one with invalid_argument. A turn of an actor that has ended refuses spawn_child before it constructs anything.

  30. A child spawned in a turn records the actor of that turn as its parent, and the parent records its children in spawn order; turn::parent returns the parent, and nothing for an actor the host spawned. turn::self is the actor handling the message.

  31. turn::stop_child(child) stops child and every actor descended from it that is still active, each after its own descendants, siblings in spawn order, and returns them in that order; their mailboxes and timers are dropped, and a member that had already ended stays as it ended while its descendants are still stopped. It is invalid_argument for anything but a child of the caller (the caller itself, a grandchild, an address that names no actor), which changes nothing. The caller is never in the family.

  32. wake_at(deadline, message, key) replaces the same actor’s timer under key; cancel_timer(key) removes it; keys belong to their actor.

  33. An actor that ends, done, error or stopped, loses its timers, and an actor that is no longer active, in the rest of the turn that ended it, arms no timer and spawns no child: wake_at and spawn_child are invalid_argument and nothing is paid. send still works.

  34. now() is the time the last clock_tick or release_next brought, 0 before any; next_deadline() the earliest deadline armed, none while no timer is. A tick earlier than the last moves now back: it releases what is due by then, and a deadline computed afterwards from now is earlier than one computed before it.

  35. turn::stop() ends the actor handling the message as stopped, alone: its children are its logic’s to stop, as after finish. It costs nothing, the rest of the turn runs as after finish, and a stop() once the actor has ended, a second one or one after finish, is invalid_argument.

  36. release_next(now, correlation) moves time to now and releases the first timer due by then, as clock_tick would release it, and says whether there was one; a host that runs the system between two releases lets a timer’s handling cancel or replace a later one of the same tick.

Reference

Every name xactor declares, in the namespace webcpp::xactor, from the Doc Comments of its headers: each class, function, type and enumerator with its declaration, what it does, what it asks of its caller and how it fails. The other sections teach the library; this one is where a name is looked up.

Macros

Name Description

WEBCPP_XACTOR_NO_EXCEPTIONS

Defined when xactor is built without exceptions: when webcpp has none (the compiler has none, or the developer defined WEBCPP_NO_EXCEPTIONS for every webcpp library), or when the developer defines it alone, to disable xactor’s in a build that has them.

webcpp::xactor

Types

Name Description

actor_logic

The behaviour of an actor, the base of the class a program writes it in.

actor_ref

An actor’s address in its scheduler.

asio_driver

The same turns, each posted as a handler onto one strand of an io_context the driver owns, and run on the calling thread by draining it with poll.

budgets

The limits a scheduler enforces, which it is constructed with.

correlation_id

The execution every message caused by one host delivery belongs to; the execution’s fuel is kept under it.

envelope

A message as the scheduler delivers it, and as actor_logic::handle receives it.

envelope_log

The entries of the messages a scheduler delivered, in order.

fifo_driver

Delivers the queued messages in FIFO order, on the calling thread.

logged_envelope

One entry of an envelope_log, a delivered envelope without its message.

scheduler

The system that holds the actors and gives each of them one message at a time, run by its host.

sequence_number

A message’s place in the order messages entered a mailbox, across every actor of a scheduler, which gives each its number as it enqueues it.

test_driver

One message per step, so a test observes exactly what one delivery changed.

turn

An actor’s turn: what it may do while it handles one message, the one delivery of the actor model.

Type Aliases

Name Description

result

What an operation that can fail returns: a T, or the error_code of the failure.

Enums

Name Description

errc

The errors xactor reports, as boost::system::error_code values of the category error_category.

status

Where an actor is in its life, which scheduler::status_of returns.

Functions

Name Description

create_actor

Runs a Logic, constructed from inputs, as a new actor of system, and returns its reference.

error_category

The category of every errc.

failure

Builds a failed result of the type the call would have returned.

make_error_code

Makes the error_code of an errc, as its implicit conversion does.

operator==

Equality operators

operator<=>

Three-way comparison operators

webcpp::xactor::result

What an operation that can fail returns: a T, or the error_code of the failure.

Synopsis

Declared in <webcpp/xactor/errors.hpp>

template<class T>
using result = boost::system::result<T, boost::system::error_code>;

Description

It is Boost.System’s result. xactor throws nothing of its own: every operation of it that can fail returns one. An operation of a scheduler or of a turn changes nothing when it fails, while a driver returns the failure that stopped it after the deliveries it has already made. Test it with has_value() before reading the value: * of a failed result is undefined, and its value() reports the failure through boost::throw_exception.

Template Parameters

Name Description

T

The type of the value; void for an operation that returns none.

webcpp::xactor::actor_logic

The behaviour of an actor, the base of the class a program writes it in.

Synopsis

template<class Message>
class actor_logic;

Description

A logic derives from it and overrides handle. The logic is not the actor: create_actor, scheduler::spawn and turn::spawn_child run a logic as an actor of a scheduler, and the running actor is the actor_ref they return. What the logic is constructed with is the actor’s input, and its members are the actor’s state. It never sees the scheduler, its mailbox or the driver, only its turn.

Member Functions

Name Description

actor_logic [constructor]

Constructors

~actor_logic [destructor] [virtual]

Destroys the logic; the scheduler that runs it does so as it is destroyed.

operator=

Assignment operators

handle [virtual]

Handles one message, the payload of cause.

Template Parameters

Name Description

Message

The type of every message of the system, the Message of the scheduler that runs the logic.

See Also

"The model", in the guide.

"Writing an actor logic", in the guide.

webcpp::xactor::actor_logic::actor_logic

Constructors

Synopses

Constructs the logic; a derived logic’s constructor takes the actor’s input.

actor_logic() = default;

Not copyable: the scheduler owns a logic where it was constructed, and never moves it.

actor_logic(actor_logic const&) = delete;

Not movable: the scheduler owns a logic where it was constructed, and never moves it.

actor_logic(actor_logic&&) = delete;

webcpp::xactor::actor_logic::actor_logic

Constructs the logic; a derived logic’s constructor takes the actor’s input.

Synopsis

actor_logic() = default;

webcpp::xactor::actor_logic::actor_logic

Not copyable: the scheduler owns a logic where it was constructed, and never moves it.

Synopsis

actor_logic(actor_logic const&) = delete;

webcpp::xactor::actor_logic::actor_logic

Not movable: the scheduler owns a logic where it was constructed, and never moves it.

Synopsis

actor_logic(actor_logic&&) = delete;

webcpp::xactor::actor_logic::~actor_logic

Destroys the logic; the scheduler that runs it does so as it is destroyed.

Synopsis

virtual
~actor_logic() = default;

webcpp::xactor::actor_logic::operator=

Assignment operators

Synopses

Not copy-assignable: the scheduler owns a logic where it was constructed, and never moves it.

actor_logic&
operator=(actor_logic const&) = delete;

Not move-assignable: the scheduler owns a logic where it was constructed, and never moves it.

webcpp::xactor::actor_logic::operator=

Not copy-assignable: the scheduler owns a logic where it was constructed, and never moves it.

Synopsis

actor_logic&
operator=(actor_logic const&) = delete;

webcpp::xactor::actor_logic::operator=

Not move-assignable: the scheduler owns a logic where it was constructed, and never moves it.

Synopsis

actor_logic&
operator=(actor_logic&&) = delete;

webcpp::xactor::actor_logic::handle

Handles one message, the payload of cause.

Synopsis

virtual
result<void>
handle(
    xactor::turn<Message>& turn,
    envelope<Message> const& cause) = 0;

Description

The scheduler calls it once for each message delivered to the actor, and delivers the actor’s next message only after it returns. Everything the actor does to the system goes through turn, which, like cause, lives only for the call.

Note

An error ends this actor and only it: every other actor keeps its turns.

Return Value

Success, or an error of any category, which ends the actor with the status status::error and is kept for scheduler::error_of, unless the turn has already ended the actor, which then stays as it ended.

Parameters

Name Description

turn

What the actor may do while it handles the message.

cause

The message, in the envelope that says who sent it and the execution it belongs to.

webcpp::xactor::actor_ref

An actor’s address in its scheduler.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

struct actor_ref;

Description

It is a number, not a handle: it keeps nothing alive, and scheduler::status_of tells whether its actor still runs. Addresses start at 1 and follow the order the actors were spawned in, by the host or by a turn, and an address is never reused: an actor that has ended keeps its address and its status. Address zero names no actor; it is the envelope::from of a message the host sent.

Data Members

Name Description

value

The address: 1 for the first actor spawned, and 0 for none.

Friends

Name Description

webcpp::xactor::operator==

Whether two addresses are the same.

webcpp::xactor::actor_ref::value

The address: 1 for the first actor spawned, and 0 for none.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

uint32_t value = 0;

webcpp::xactor::asio_driver

The same turns, each posted as a handler onto one strand of an io_context the driver owns, and run on the calling thread by draining it with poll.

Synopsis

template<class Message>
class asio_driver;

Description

The context is the driver’s alone: it offers no executor, so the program’s own I/O objects run on a context of their own, never on the driver’s.

It is native only: a WASI build does not declare it. For the same deliveries, its envelope log equals the log of a fifo_driver.

Note

The context is drained with poll under a work guard: the guard keeps the context from running out of work, so run would never return, and run_one and run_for would block waiting for more.

Member Functions

Name Description

asio_driver [constructor]

Makes a driver of owner, with its io_context.

run_until_idle

Delivers until nothing is left, each turn posted onto the strand, and returns how many were delivered.

Template Parameters

Name Description

Message

The type of every message of the system.

See Also

"The Asio driver", in the guide.

webcpp::xactor::asio_driver::asio_driver

Makes a driver of owner, with its io_context.

Synopsis

explicit
asio_driver(scheduler<Message>& owner);

Parameters

Name Description

owner

The scheduler whose turns it runs.

Preconditions

  • owner outlives the driver.

webcpp::xactor::asio_driver::run_until_idle

Delivers until nothing is left, each turn posted onto the strand, and returns how many were delivered.

Synopsis

result<std::size_t>
run_until_idle(envelope_log* log);

Return Value

How many messages it delivered, the scheduler then idle; or the first failure of scheduler::run_one, which stops it.

Parameters

Name Description

log

The log that records each delivery, or nullptr for none.

webcpp::xactor::budgets

The limits a scheduler enforces, which it is constructed with.

Synopsis

struct budgets;

Data Members

Name Description

fuel

The fuel of one execution: every message its actors send, every timer they arm and every unit they spend is paid from it.

See Also

"Fuel", in the guide.

webcpp::xactor::budgets::fuel

The fuel of one execution: every message its actors send, every timer they arm and every unit they spend is paid from it.

Synopsis

uint32_t fuel = 1'000'000;

Description

An execution starts with the whole of it and is never refilled, so the work of one execution is bounded, whatever its actors send.

webcpp::xactor::correlation_id

The execution every message caused by one host delivery belongs to; the execution’s fuel is kept under it.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

struct correlation_id;

Description

The host names it with each scheduler::deliver, scheduler::clock_tick and scheduler::release_next, and every message sent in the turns that follow belongs to it. The host chooses the values: a delivery with a correlation already used shares what its execution has left, so a host gives each input a correlation of its own.

Data Members

Name Description

value

The number the host gave the execution.

Friends

Name Description

webcpp::xactor::operator<=>

Orders two correlations by their values, so that one can key an ordered container.

webcpp::xactor::operator==

Whether two correlations name the same execution.

webcpp::xactor::correlation_id::value

The number the host gave the execution.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

uint64_t value = 0;

webcpp::xactor::envelope

A message as the scheduler delivers it, and as actor_logic::handle receives it.

Synopsis

template<class Message>
struct envelope;

Description

The scheduler makes every envelope; a program reads them.

Data Members

Name Description

correlation

The execution it belongs to, which pays for what its handling sends.

from

The actor that sent it: address zero when the host sent it, and the actor itself for the message of one of its timers.

payload

The message.

sequence

Its place in the order messages entered a mailbox, across every actor of the scheduler: 1 for the first.

to

The actor it is for.

Template Parameters

Name Description

Message

The type of every message of the system.

webcpp::xactor::envelope::correlation

The execution it belongs to, which pays for what its handling sends.

Synopsis

correlation_id correlation = {};

webcpp::xactor::envelope::from

The actor that sent it: address zero when the host sent it, and the actor itself for the message of one of its timers.

Synopsis

actor_ref from = {};

webcpp::xactor::envelope::payload

The message.

Synopsis

Message payload = {};

webcpp::xactor::envelope::sequence

Its place in the order messages entered a mailbox, across every actor of the scheduler: 1 for the first.

Synopsis

sequence_number sequence = {};

webcpp::xactor::envelope::to

The actor it is for.

Synopsis

actor_ref to = {};

webcpp::xactor::envelope_log

The entries of the messages a scheduler delivered, in order.

Synopsis

class envelope_log;

Description

scheduler::run_one records each message it delivers, before its actor handles it, in the log it is given, and nothing for a message dropped. For the same budgets and deliveries every driver records the same log, so a program that runs the same deliveries twice and compares the logs checks that its own actors keep a run deterministic.

Member Functions

Name Description

entries

The entries, in the order they were recorded.

record

Appends the entry of a delivered message; scheduler::run_one calls it.

Friends

Name Description

webcpp::xactor::operator==

Whether two logs hold equal entries, in the same order.

See Also

"The envelope log", in the guide.

webcpp::xactor::envelope_log::entries

The entries, in the order they were recorded.

Synopsis

[[nodiscard]]
std::vector<logged_envelope> const&
entries() const noexcept;

Return Value

The entries, one for each delivery recorded in this log: a scheduler::run_one given nullptr, or another log, records nothing here.

Note

The return value should not be discarded.

webcpp::xactor::envelope_log::record

Appends the entry of a delivered message; scheduler::run_one calls it.

Synopsis

template<class Message>
void
record(
    envelope<Message> const& delivered,
    uint32_t remaining_fuel);

Template Parameters

Name Description

Message

The type of every message of the system, a std::variant, whose index() the entry keeps.

Parameters

Name Description

delivered

The envelope delivered.

remaining_fuel

The fuel its execution had when it was delivered.

webcpp::xactor::fifo_driver

Delivers the queued messages in FIFO order, on the calling thread.

Synopsis

template<class Message>
class fifo_driver;

Description

It is the driver of a program, native or WASI: each delivery is one scheduler::run_one of the scheduler it holds.

Member Functions

Name Description

fifo_driver [constructor]

Makes a driver of owner.

run_turn

Delivers at most budget messages, for a host that has to take the thread back between turns.

run_until_idle

Delivers until nothing is left and returns how many were delivered.

Template Parameters

Name Description

Message

The type of every message of the system.

See Also

"Drivers", in the guide.

webcpp::xactor::fifo_driver::fifo_driver

Makes a driver of owner.

Synopsis

explicit
fifo_driver(scheduler<Message>& owner) noexcept;

Parameters

Name Description

owner

The scheduler whose turns it runs.

Preconditions

  • owner outlives the driver.

webcpp::xactor::fifo_driver::run_turn

Delivers at most budget messages, for a host that has to take the thread back between turns.

Synopsis

result<std::size_t>
run_turn(
    std::size_t budget,
    envelope_log* log);

Return Value

How many messages it delivered, fewer than budget when the messages ran out first; or the first failure of scheduler::run_one, which stops it.

Parameters

Name Description

budget

The most messages it delivers.

log

The log that records each delivery, or nullptr for none.

webcpp::xactor::fifo_driver::run_until_idle

Delivers until nothing is left and returns how many were delivered.

Synopsis

result<std::size_t>
run_until_idle(envelope_log* log);

Description

Every message a turn sends is paid from its execution’s fuel, so the messages run out.

Return Value

How many messages it delivered, the scheduler then idle; or the first failure of scheduler::run_one, which stops it.

Parameters

Name Description

log

The log that records each delivery, or nullptr for none.

webcpp::xactor::logged_envelope

One entry of an envelope_log, a delivered envelope without its message.

Synopsis

struct logged_envelope;

Description

Note

It keeps which alternative of the message variant arrived, not the message, so an entry has a fixed width, the log is one type whatever the Message, and two logs compare cheaply.

Data Members

Name Description

correlation

The execution it belonged to.

from

The actor that sent it: address zero when the host did, and the actor itself for the message of one of its timers.

payload_index

Which alternative of the message variant it was, its index().

remaining_fuel

The execution’s fuel when the message was delivered, before its actor handled it.

sequence

The delivered envelope’s envelope::sequence.

to

The actor it was delivered to.

Friends

Name Description

webcpp::xactor::operator==

Whether two entries are equal, member by member.

webcpp::xactor::logged_envelope::correlation

The execution it belonged to.

Synopsis

correlation_id correlation = {};

webcpp::xactor::logged_envelope::from

The actor that sent it: address zero when the host did, and the actor itself for the message of one of its timers.

Synopsis

actor_ref from = {};

webcpp::xactor::logged_envelope::payload_index

Which alternative of the message variant it was, its index().

Synopsis

std::size_t payload_index = 0;

webcpp::xactor::logged_envelope::remaining_fuel

The execution’s fuel when the message was delivered, before its actor handled it.

Synopsis

uint32_t remaining_fuel = 0;

webcpp::xactor::logged_envelope::sequence

The delivered envelope’s envelope::sequence.

Synopsis

sequence_number sequence = {};

webcpp::xactor::logged_envelope::to

The actor it was delivered to.

Synopsis

actor_ref to = {};

webcpp::xactor::scheduler

The system that holds the actors and gives each of them one message at a time, run by its host.

Synopsis

template<class Message>
class scheduler;

Description

Each actor has a FIFO mailbox, and the actors with messages waiting wait in one FIFO queue, each at most once: run_one takes the actor at the front, hands it the first message of its mailbox, and puts it back at the end while its mailbox holds more. A message an actor sends waits in a mailbox, and is never handled inside the sender’s turn. The scheduler keeps each actor’s logic, status, output, error and family for as long as it lives. It starts no thread and reads no clock: its time is what the host brings.

Note

One FIFO mailbox per actor and one FIFO queue of actors with work make the delivery order a function of the enqueue order alone, which is what makes a run deterministic and lets two drivers produce one log. Fuel is kept per execution: every message caused by one host delivery, whatever actor sends it, is paid from that delivery’s execution.

Member Functions

Name Description

scheduler [constructor]

Makes a system with no actor, at the time 0, whose executions each start with limits.fuel.

clock_tick

Moves time to now: releases every timer whose deadline has been reached, in (deadline, arming) order, into the execution correlation.

deliver

Puts a message from the host in an actor’s mailbox; it runs when a driver delivers it.

error_of

The error an actor’s handler returned, or an empty code.

fuel_of

The fuel an execution has left; the whole budget for one not yet opened.

idle

Whether no message waits in any mailbox.

limits

The budgets the scheduler was constructed with.

next_deadline

The earliest deadline of an armed timer; none while no timer is armed.

now

The time the last clock_tick or release_next brought; 0 before any.

output_of

The output an actor finished with, or nullptr while it has none.

pending_timers

How many timers are armed and not yet released.

release_next

Moves time to now and releases the first timer due by then, in (deadline, arming) order, into the execution correlation.

run_one

Delivers exactly one message to its actor and returns whether there was one.

spawn

Takes an actor into the system and gives it its address.

status_of

Where an actor is in its life.

stop

Ends an actor from outside with its whole family, as turn::stop_child does.

Friends

Name Description

webcpp::xactor::turn

An actor’s turn: what it may do while it handles one message, the one delivery of the actor model.

Template Parameters

Name Description

Message

The type of every message of the system: a std::variant that can be moved, whose index() the envelope log records.

See Also

"The scheduler", in the guide.

webcpp::xactor::scheduler::scheduler

Makes a system with no actor, at the time 0, whose executions each start with limits.fuel.

Synopsis

explicit
scheduler(budgets limits) noexcept;

Parameters

Name Description

limits

The budgets it enforces.

webcpp::xactor::scheduler::clock_tick

Moves time to now: releases every timer whose deadline has been reached, in (deadline, arming) order, into the execution correlation.

Synopsis

result<void>
clock_tick(
    uint64_t now,
    correlation_id correlation);

Description

Each timer’s message enters its actor’s mailbox, its sender the actor itself; releasing costs no fuel, since arming was paid. A time earlier than now is not refused: time moves back. The call opens the execution correlation when it is new, as deliver does.

Note

Time never advances by itself; the host brings it.

Return Value

Success, always.

Parameters

Name Description

now

The time the host brings.

correlation

The execution the released messages belong to.

webcpp::xactor::scheduler::deliver

Puts a message from the host in an actor’s mailbox; it runs when a driver delivers it.

Synopsis

result<void>
deliver(
    actor_ref to,
    Message payload,
    correlation_id correlation);

Description

The message’s envelope::from is address zero. A message to an actor that has ended is dropped, and the call succeeds.

Note

The first call that names a correlation, this one, clock_tick or release_next, opens its execution with the budget’s fuel; a host delivery costs no fuel.

Return Value

Success; errc::invalid_argument when to names no actor, which changes nothing.

Parameters

Name Description

to

The actor it is for.

payload

The message.

correlation

The execution it belongs to.

webcpp::xactor::scheduler::error_of

The error an actor’s handler returned, or an empty code.

Synopsis

[[nodiscard]]
boost::system::error_code
error_of(actor_ref address) const;

Return Value

The error, as the handler returned it; an empty code unless its status is status::error, and for an address that names no actor.

Note

The return value should not be discarded.

Parameters

Name Description

address

The actor.

webcpp::xactor::scheduler::fuel_of

The fuel an execution has left; the whole budget for one not yet opened.

Synopsis

[[nodiscard]]
uint32_t
fuel_of(correlation_id correlation) const;

Return Value

The fuel it has left.

Note

The return value should not be discarded.

Parameters

Name Description

correlation

The execution.

webcpp::xactor::scheduler::idle

Whether no message waits in any mailbox.

Synopsis

[[nodiscard]]
bool
idle() const noexcept;

Return Value

true when run_one would deliver nothing.

Note

The return value should not be discarded.

webcpp::xactor::scheduler::limits

The budgets the scheduler was constructed with.

Synopsis

[[nodiscard]]
budgets const&
limits() const noexcept;

Return Value

The budgets.

Note

The return value should not be discarded.

webcpp::xactor::scheduler::next_deadline

The earliest deadline of an armed timer; none while no timer is armed.

Synopsis

[[nodiscard]]
std::optional<uint64_t>
next_deadline() const noexcept;

Return Value

The earliest deadline, or std::nullopt.

Note

The return value should not be discarded.

webcpp::xactor::scheduler::now

The time the last clock_tick or release_next brought; 0 before any.

Synopsis

[[nodiscard]]
uint64_t
now() const noexcept;

Return Value

The time.

Note

The return value should not be discarded.

webcpp::xactor::scheduler::output_of

The output an actor finished with, or nullptr while it has none.

Synopsis

[[nodiscard]]
Message const*
output_of(actor_ref address) const;

Description

Note

The pointer stays valid until the scheduler takes in another actor.

Return Value

The output its turn gave turn::finish; nullptr unless its status is status::done, and for an address that names no actor.

Note

The return value should not be discarded.

Parameters

Name Description

address

The actor.

webcpp::xactor::scheduler::pending_timers

How many timers are armed and not yet released.

Synopsis

[[nodiscard]]
std::size_t
pending_timers() const noexcept;

Return Value

The number of armed timers.

Note

The return value should not be discarded.

webcpp::xactor::scheduler::release_next

Moves time to now and releases the first timer due by then, in (deadline, arming) order, into the execution correlation.

Synopsis

result<bool>
release_next(
    uint64_t now,
    correlation_id correlation);

Description

The timer is released as clock_tick releases each of its own.

Note

A caller that runs the system between two releases lets a timer’s handling cancel or replace a later timer of the same tick, which clock_tick, releasing them all at once, cannot.

Return Value

true when it released a timer, false when none is due.

Parameters

Name Description

now

The time the host brings.

correlation

The execution the released message belongs to.

webcpp::xactor::scheduler::run_one

Delivers exactly one message to its actor and returns whether there was one.

Synopsis

result<bool>
run_one(envelope_log* log);

Description

It takes the actor at the front of the queue, puts it back at the end while its mailbox holds more, records the message in log, when it is not nullptr, with the fuel its execution has, and calls the actor’s actor_logic::handle with the message and a turn. A handler’s error ends its actor with the status status::error, unless the turn has already ended it.

Note

A handler’s error ends its actor, not the run, so this returns no error today; it returns a result so that a failure of the scheduler itself has a channel the drivers already forward.

Return Value

true when it delivered a message, false when none was waiting.

Parameters

Name Description

log

The log that records the delivery, or nullptr for none.

webcpp::xactor::scheduler::spawn

Takes an actor into the system and gives it its address.

Synopsis

result<actor_ref>
spawn(std::unique_ptr<actor_logic<Message>> spawned);

Description

The actor starts status::active, with no parent, at the next address: addresses start at one. A test that keeps a pointer to the logic reads its members after each delivery: the scheduler never moves it.

Return Value

The new actor’s address; errc::invalid_argument for a null spawned, which takes no address.

Parameters

Name Description

spawned

The actor’s logic, which the scheduler owns from then on.

webcpp::xactor::scheduler::status_of

Where an actor is in its life.

Synopsis

[[nodiscard]]
result<status>
status_of(actor_ref address) const;

Return Value

Its status; errc::invalid_argument when address names no actor.

Note

The return value should not be discarded.

Parameters

Name Description

address

The actor.

webcpp::xactor::scheduler::stop

Ends an actor from outside with its whole family, as turn::stop_child does.

Description

Each member that is still active is stopped after its own descendants, siblings in spawn order, and its pending messages and timers are dropped; a member that has already ended stays as it ended, and its descendants are stopped all the same.

Return Value

The actors it stopped, in the order it stopped them; errc::invalid_argument when address names no actor, which changes nothing.

Parameters

Name Description

address

The actor to stop, with its family.

webcpp::xactor::sequence_number

A message’s place in the order messages entered a mailbox, across every actor of a scheduler, which gives each its number as it enqueues it.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

struct sequence_number;

Member Functions

Name Description

next

The number after this one.

Data Members

Name Description

value

The place, counted from 1 for the first message enqueued.

Friends

Name Description

webcpp::xactor::operator<=>

Orders two places, the earlier first.

webcpp::xactor::operator==

Whether two places are the same.

webcpp::xactor::sequence_number::next

The number after this one.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

[[nodiscard]]
sequence_number
next() const noexcept;

Return Value

The sequence number whose value is one more than this one’s.

Note

The return value should not be discarded.

webcpp::xactor::sequence_number::value

The place, counted from 1 for the first message enqueued.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

uint64_t value = 0;

webcpp::xactor::test_driver

One message per step, so a test observes exactly what one delivery changed.

Synopsis

template<class Message>
class test_driver;

Description

A step is one scheduler::run_one, and the driver has nothing else: the host brings messages with scheduler::deliver, and time with scheduler::clock_tick or scheduler::release_next.

Member Functions

Name Description

test_driver [constructor]

Makes a driver of owner.

step

step overloads

Template Parameters

Name Description

Message

The type of every message of the system.

See Also

"Drivers", in the guide.

webcpp::xactor::test_driver::test_driver

Makes a driver of owner.

Synopsis

explicit
test_driver(scheduler<Message>& owner) noexcept;

Parameters

Name Description

owner

The scheduler whose turns it runs.

Preconditions

  • owner outlives the driver.

webcpp::xactor::test_driver::step

step overloads

Synopses

Delivers one message, when one is waiting, and records nothing.

Delivers one message, when one is waiting, and records it in log.

Return Value

Whether there was a message to deliver; or a failure of scheduler::run_one.

Parameters

Name Description

log

The log that records the delivery.

webcpp::xactor::test_driver::step

Delivers one message, when one is waiting, and records nothing.

Synopsis

result<bool>
step();

Return Value

Whether there was a message to deliver; or a failure of scheduler::run_one.

webcpp::xactor::test_driver::step

Delivers one message, when one is waiting, and records it in log.

Synopsis

Return Value

Whether there was a message to deliver; or a failure of scheduler::run_one.

Parameters

Name Description

log

The log that records the delivery.

webcpp::xactor::turn

An actor’s turn: what it may do while it handles one message, the one delivery of the actor model.

Synopsis

template<class Message>
class turn;

Description

Every message it sends, every timer it arms and every unit it spends is paid from the fuel of the execution the message belongs to, and what the execution cannot pay fails with errc::resource_limit and changes nothing. When finish or stop ends the actor, the rest of the turn still runs: the actor receives nothing more, and it can still send, but it arms no timer and spawns no child.

Note

It lives for one handle and holds references to the scheduler and to the message, so a logic never keeps it.

Member Functions

Name Description

turn [constructor]

Makes the turn of the actor cause is for.

cancel_timer

Removes this actor’s timer under key, if it has one.

finish

Ends this actor with its output: it becomes done and receives nothing more.

fuel

The fuel this execution has left.

now

The time the last scheduler::clock_tick or scheduler::release_next brought.

parent

The actor whose turn spawned this one; none for one the host spawned.

reply

Sends a message to whoever sent the one being handled.

self

This actor’s own reference.

send

Sends a message to an actor, for one unit of fuel.

spawn_child

Runs a Logic, constructed from inputs, as a new actor of this system, a child of this one, and returns its reference.

spend

Pays units of fuel for work this actor does itself, all of them or none.

stop

Ends this actor as stopped: nothing more reaches it, and its timers are dropped.

stop_child

Stops a child of this actor and every active actor descended from it.

wake_at

wake_at overloads

Template Parameters

Name Description

Message

The type of every message of the system.

See Also

"Turns", in the guide.

webcpp::xactor::turn::turn

Makes the turn of the actor cause is for.

Synopsis

turn(
    scheduler<Message>& owner,
    envelope<Message> const& cause) noexcept;

Description

scheduler::run_one makes one for each message it delivers, and a program makes none.

Parameters

Name Description

owner

The scheduler that runs the actor.

cause

The envelope of the message being handled.

Preconditions

  • owner and cause outlive the turn, and cause.to names an actor of owner.

webcpp::xactor::turn::cancel_timer

Removes this actor’s timer under key, if it has one.

Synopsis

void
cancel_timer(std::string_view key);

Description

It costs no fuel.

Parameters

Name Description

key

The key the timer was armed under.

webcpp::xactor::turn::finish

Ends this actor with its output: it becomes done and receives nothing more.

Synopsis

result<void>
finish(Message output);

Description

Its timers are dropped, its children run on, and scheduler::output_of returns the output. It costs no fuel.

Return Value

Success; errc::invalid_argument, which changes nothing, once this actor has ended: an actor finishes once.

Parameters

Name Description

output

The actor’s output.

webcpp::xactor::turn::fuel

The fuel this execution has left.

Synopsis

[[nodiscard]]
uint32_t
fuel() const;

Return Value

The fuel the execution of the message being handled has left.

Note

The return value should not be discarded.

webcpp::xactor::turn::now

The time the last scheduler::clock_tick or scheduler::release_next brought.

Synopsis

[[nodiscard]]
uint64_t
now() const noexcept;

Return Value

The time, as scheduler::now returns it; 0 before any.

Note

The return value should not be discarded.

webcpp::xactor::turn::parent

The actor whose turn spawned this one; none for one the host spawned.

Synopsis

[[nodiscard]]
std::optional<actor_ref>
parent() const;

Return Value

The parent’s address, or std::nullopt.

Note

The return value should not be discarded.

webcpp::xactor::turn::reply

Sends a message to whoever sent the one being handled.

Synopsis

result<void>
reply(Message payload);

Description

It sends as send does: to this actor itself for the message of one of its timers.

Return Value

Success; errc::invalid_argument, and nothing paid, for a message from the host, which has no sender; errc::resource_limit when the execution has no fuel left.

Parameters

Name Description

payload

The message.

webcpp::xactor::turn::self

This actor’s own reference.

Synopsis

[[nodiscard]]
actor_ref
self() const noexcept;

Return Value

The address of the actor handling the message.

Note

The return value should not be discarded.

webcpp::xactor::turn::send

Sends a message to an actor, for one unit of fuel.

Synopsis

result<void>
send(
    actor_ref to,
    Message payload);

Description

The message waits in the mailbox of to, its sender this actor, and is handled in a turn of its own, later. A message to an actor that has ended is paid and dropped, and the call succeeds.

Return Value

Success; errc::invalid_argument, and nothing paid, when to names no actor; errc::resource_limit when the execution has no fuel left.

Parameters

Name Description

to

The actor it is for, this one included.

payload

The message.

webcpp::xactor::turn::spawn_child

Runs a Logic, constructed from inputs, as a new actor of this system, a child of this one, and returns its reference.

Synopsis

template<
    class Logic,
    class... Inputs>
result<actor_ref>
spawn_child(Inputs&&... inputs);

Description

The child is status::active at once, at the next address. It records this actor as its parent, and this actor records its children in spawn order. It costs no fuel.

Return Value

The child’s address; errc::invalid_argument once this actor has ended, before any Logic is constructed.

Template Parameters

Name Description

Logic

The child’s logic, a class derived from actor_logic of Message.

Inputs

The types of the arguments a Logic is constructed from.

Parameters

Name Description

inputs

The arguments a Logic is constructed from, forwarded: the child’s input.

webcpp::xactor::turn::spend

Pays units of fuel for work this actor does itself, all of them or none.

Synopsis

result<void>
spend(uint32_t units);

Description

It lets an actor that computes without sending bound its work too.

Return Value

Success; errc::resource_limit, and nothing spent, when the execution has fewer left.

Parameters

Name Description

units

The fuel to pay.

webcpp::xactor::turn::stop

Ends this actor as stopped: nothing more reaches it, and its timers are dropped.

Synopsis

result<void>
stop();

Description

It ends alone: its children are its logic’s to stop, with stop_child, as after finish. It costs no fuel.

Note

An actor its parent asks to stop with a message has handled what its parent sent it before, and may ask its own children the same way.

Return Value

Success; errc::invalid_argument once this actor has ended.

webcpp::xactor::turn::stop_child

Stops a child of this actor and every active actor descended from it.

Synopsis

Description

Each is stopped after its own descendants, siblings in spawn order, and its pending messages and timers are dropped; a member that has already ended stays as it ended, and its descendants are stopped all the same. It costs no fuel.

Return Value

The actors it stopped, in the order it stopped them; errc::invalid_argument, which changes nothing, for anything but a child of this actor: the actor itself, a grandchild, an address that names no actor.

Parameters

Name Description

child

The child to stop, with its family.

webcpp::xactor::turn::wake_at

wake_at overloads

Synopses

Arms a timer on this actor, for one unit of fuel.

result<void>
wake_at(
    uint64_t deadline,
    Message payload);

Arms a timer under key, for one unit of fuel, replacing this actor’s timer with the same key.

result<void>
wake_at(
    uint64_t deadline,
    Message payload,
    std::string key);

Return Value

Parameters

Name Description

deadline

The time it fires at.

payload

The message it brings back.

key

The key the timer is armed under.

See Also

"Timers", in the guide.

webcpp::xactor::turn::wake_at

Arms a timer on this actor, for one unit of fuel.

Synopsis

result<void>
wake_at(
    uint64_t deadline,
    Message payload);

Description

payload comes back to this actor, its sender the actor itself, when the host’s scheduler::clock_tick or scheduler::release_next reaches deadline; timers with one deadline are released in the order they were armed. A deadline is a time, so a delay is now() + delay.

Return Value

Success; errc::invalid_argument, and nothing paid, once this actor has ended; errc::resource_limit when the execution has no fuel left.

Parameters

Name Description

deadline

The time it fires at.

payload

The message it brings back.

See Also

"Timers", in the guide.

webcpp::xactor::turn::wake_at

Arms a timer under key, for one unit of fuel, replacing this actor’s timer with the same key.

Synopsis

result<void>
wake_at(
    uint64_t deadline,
    Message payload,
    std::string key);

Description

It fires as a timer without a key does, and the replacement takes a new arming order. Keys belong to their actor, so two actors may use the same one.

Return Value

Success; errc::invalid_argument, and nothing paid, once this actor has ended; errc::resource_limit when the execution has no fuel left. A call refused leaves the timer under key armed.

Parameters

Name Description

deadline

The time it fires at.

payload

The message it brings back.

key

The key the timer is armed under.

See Also

"Timers", in the guide.

webcpp::xactor::errc

The errors xactor reports, as boost::system::error_code values of the category error_category.

Synopsis

Declared in <webcpp/xactor/errors.hpp>

enum class errc : int;

Description

The values start at 1, so an error_code of value 0, which scheduler::error_of returns for an actor that has not failed, is no error. An errc converts to an error_code and compares with one: sent.error() == xactor::errc::resource_limit.

Note

A handler may return any error_code, of this category or of its own; these two are the system’s own.

Members

Name Description

invalid_argument

A call that names what it cannot act on, which changes nothing.

resource_limit

The execution has no fuel left for what was asked, which changes nothing: nothing is enqueued, armed or spent.

webcpp::xactor::errc::invalid_argument

A call that names what it cannot act on, which changes nothing.

Synopsis

Declared in <webcpp/xactor/errors.hpp>

invalid_argument = 1

Description

A null logic given to scheduler::spawn; an address that names no actor, given to scheduler::deliver, scheduler::stop, scheduler::status_of or turn::send; a turn::reply to a message from the host, which has no sender; a turn::finish or a turn::stop once the actor has ended; a turn::stop_child of an actor that is not a child; or a timer or a child asked by an actor that has ended.

webcpp::xactor::errc::resource_limit

The execution has no fuel left for what was asked, which changes nothing: nothing is enqueued, armed or spent.

Synopsis

Declared in <webcpp/xactor/errors.hpp>

resource_limit = 2

webcpp::xactor::status

Where an actor is in its life, which scheduler::status_of returns.

Synopsis

Declared in <webcpp/xactor/status.hpp>

enum class status : uint8_t;

Description

An actor starts active, the one status in which it receives messages, and ends in one of the three others, each final: its pending messages and its timers are dropped, and every later message to it is dropped too.

Note

The values are fixed and never reused, because a system that persists its actors rebuilds them from these numbers.

Members

Name Description

active

Receives messages; every actor starts here.

done

Finished with an output, through turn::finish, which scheduler::output_of returns.

error

Its handler returned an error, which scheduler::error_of returns.

stopped

Stopped by scheduler::stop or its parent’s turn::stop_child, alone or with an ancestor, or by its own turn::stop.

See Also

"Lifecycle", in the guide.

webcpp::xactor::status::active

Receives messages; every actor starts here.

Synopsis

Declared in <webcpp/xactor/status.hpp>

active = 1

webcpp::xactor::status::done

Finished with an output, through turn::finish, which scheduler::output_of returns.

Synopsis

Declared in <webcpp/xactor/status.hpp>

done = 2

webcpp::xactor::status::error

Its handler returned an error, which scheduler::error_of returns.

Synopsis

Declared in <webcpp/xactor/status.hpp>

error = 3

webcpp::xactor::status::stopped

Stopped by scheduler::stop or its parent’s turn::stop_child, alone or with an ancestor, or by its own turn::stop.

Synopsis

Declared in <webcpp/xactor/status.hpp>

stopped = 4

webcpp::xactor::create_actor

Runs a Logic, constructed from inputs, as a new actor of system, and returns its reference.

Synopsis

template<
    class Logic,
    class Message,
    class... Inputs>
result<actor_ref>
create_actor(
    scheduler<Message>& system,
    Inputs&&... inputs);

Description

The actor has no parent and takes the next address. Message is deduced from system, so a call names the logic alone: xactor::create_actor<counter>(system, echo).

Note

The actor is active at once, with no separate start: a caller that needs one starts it by delivering the actor its first message.

Return Value

The new actor’s address. It never fails: the logic it gives scheduler::spawn is never null.

Template Parameters

Name Description

Logic

The actor’s logic, a class derived from actor_logic of Message.

Message

The type of every message of the system, deduced from system.

Inputs

The types of the arguments a Logic is constructed from.

Parameters

Name Description

system

The scheduler that runs the actor.

inputs

The arguments a Logic is constructed from, forwarded: the actor’s input.

webcpp::xactor::error_category

The category of every errc.

Synopsis

Declared in <webcpp/xactor/errors.hpp>

boost::system::error_category const&
error_category() noexcept;

Description

Its name() is "webcpp.xactor", and its message(value) the name of the enumerator, "resource_limit" for errc::resource_limit, or "unknown" for a value errc does not define.

Return Value

The category, a constant with a fixed identity.

webcpp::xactor::failure

Builds a failed result of the type the call would have returned.

Synopsis

Declared in <webcpp/xactor/errors.hpp>

template<class T>
result<T>
failure(errc code) noexcept;

Return Value

A result that holds make_error_code(code).

Template Parameters

Name Description

T

The type of the value the result would hold.

Parameters

Name Description

code

The error.

webcpp::xactor::make_error_code

Makes the error_code of an errc, as its implicit conversion does.

Synopsis

Declared in <webcpp/xactor/errors.hpp>

boost::system::error_code
make_error_code(errc code) noexcept;

Return Value

code as an error_code of the category error_category.

Parameters

Name Description

code

The error.

webcpp::xactor::operator==

Equality operators

Synopses

Whether two addresses are the same.

constexpr
bool
operator==(
    actor_ref,
    actor_ref) = default;

Whether two correlations name the same execution.

constexpr
bool
operator==(
    correlation_id,
    correlation_id) = default;

Whether two places are the same.

constexpr
bool
operator==(
    sequence_number,
    sequence_number) = default;

Whether two entries are equal, member by member.

constexpr
bool
operator==(
    logged_envelope const&,
    logged_envelope const&) = default;

Whether two logs hold equal entries, in the same order.

bool
operator==(
    envelope_log const& left,
    envelope_log const& right);

Return Value

  • true when both name the same actor, or both name none.

  • true when their values are equal.

  • true when every member of one equals the same member of the other.

  • true when both hold the same entries in the same order.

Parameters

Name Description

left

One log.

right

The other log.

webcpp::xactor::operator==

Whether two addresses are the same.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

constexpr
bool
operator==(
    actor_ref,
    actor_ref) = default;
Note

This function is a hidden friend of actor_ref, declared only inside that class: it can be found only through argument-dependent lookup.

Return Value

true when both name the same actor, or both name none.

webcpp::xactor::operator==

Whether two correlations name the same execution.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

constexpr
bool
operator==(
    correlation_id,
    correlation_id) = default;
Note

This function is a hidden friend of correlation_id, declared only inside that class: it can be found only through argument-dependent lookup.

Return Value

true when their values are equal.

webcpp::xactor::operator==

Whether two places are the same.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

constexpr
bool
operator==(
    sequence_number,
    sequence_number) = default;
Note

This function is a hidden friend of sequence_number, declared only inside that class: it can be found only through argument-dependent lookup.

Return Value

true when their values are equal.

webcpp::xactor::operator==

Whether two entries are equal, member by member.

Synopsis

constexpr
bool
operator==(
    logged_envelope const&,
    logged_envelope const&) = default;
Note

This function is a hidden friend of logged_envelope, declared only inside that class: it can be found only through argument-dependent lookup.

Return Value

true when every member of one equals the same member of the other.

webcpp::xactor::operator==

Whether two logs hold equal entries, in the same order.

Synopsis

bool
operator==(
    envelope_log const& left,
    envelope_log const& right);
Note

This function is a hidden friend of envelope_log, declared only inside that class: it can be found only through argument-dependent lookup.

Return Value

true when both hold the same entries in the same order.

Parameters

Name Description

left

One log.

right

The other log.

webcpp::xactor::operator<=>

Three-way comparison operators

Synopses

Declared in <webcpp/xactor/ids.hpp>

Orders two correlations by their values, so that one can key an ordered container.

constexpr
auto
operator<=>(
    correlation_id,
    correlation_id) = default;

Orders two places, the earlier first.

constexpr
auto
operator<=>(
    sequence_number,
    sequence_number) = default;

Return Value

The order of their values.

webcpp::xactor::operator<=>

Orders two correlations by their values, so that one can key an ordered container.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

constexpr
auto
operator<=>(
    correlation_id,
    correlation_id) = default;
Note

This function is a hidden friend of correlation_id, declared only inside that class: it can be found only through argument-dependent lookup.

Return Value

The order of their values.

webcpp::xactor::operator<=>

Orders two places, the earlier first.

Synopsis

Declared in <webcpp/xactor/ids.hpp>

constexpr
auto
operator<=>(
    sequence_number,
    sequence_number) = default;
Note

This function is a hidden friend of sequence_number, declared only inside that class: it can be found only through argument-dependent lookup.

Return Value

The order of their values.

WEBCPP_XACTOR_NO_EXCEPTIONS

Defined when xactor is built without exceptions: when webcpp has none (the compiler has none, or the developer defined WEBCPP_NO_EXCEPTIONS for every webcpp library), or when the developer defines it alone, to disable xactor’s in a build that has them.

Synopsis

Declared in <webcpp/xactor/config.hpp>

#define WEBCPP_XACTOR_NO_EXCEPTIONS

Description

xactor raises no exception of its own: every operation that can fail returns a result, so the macro changes nothing in it. It is provided so that a program can set it for every webcpp library alike. An exception xactor raised would go through boost::throw_exception, and an API that throws would be absent while this is defined.

See Also

"Targets", in the guide.

Appendix A: Glossary

The terms this page uses, in alphabetical order. Each entry says what the term means here, what it is in the code, and which section explains it.

Actor

An independent object that reaches others only by messages: it has its own state, which only it changes, and it handles one message at a time, from its mailbox. In xactor, an actor is a logic running in a scheduler, at an address, a number from 1 in the order the actors were created.

In the code: an xactor::actor_ref, which create_actor, the scheduler’s spawn and a turn’s spawn_child return.
See The model.

Actor logic

What an actor does, apart from the actor that runs it: a class with one handle, called with one message at a time and the turn it arrived in, through which it does everything it does. It never sees the scheduler, its mailbox or the driver. Its members are the actor’s state, and only its own handle changes them.

In the code: a class derived from xactor’s actor_logic<Message>, whose handle takes the turn and the envelope and returns an xactor::result<void>.
See Writing an actor logic.

Driver

What decides when turns happen: it hands the queued messages to their actors, one per turn, until none is left, or until it has handed as many as its host allows. Every driver delivers the same messages in the same order; they differ in who holds the thread.

In the code: fifo_driver for a program, test_driver for a test, one message per step(), and asio_driver for a native program that runs its turns as Boost.Asio handlers; each calls the scheduler’s run_one.
See Drivers.

Envelope

A message as xactor carries it, and as an actor logic receives it: the actor it is for, the actor that sent it, the execution it belongs to, its sequence number, in the order messages entered a mailbox, and the message itself.

In the code: xactor::envelope<Message> (to, from, correlation, sequence, payload); from is the address 0 for the host.
See The model.

Envelope log

The record of the messages a run delivered, one entry for each, written before the actor handles it. It keeps which alternative of the message variant arrived, not the message, so two logs compare cheaply: two schedulers built with the same budgets and fed the same deliveries record equal logs, whatever driver runs them.

In the code: xactor::envelope_log, given to run_one or to a driver, whose entries() are each a logged_envelope (sequence, to, from, correlation, remaining_fuel, payload_index).
See The envelope log.

Execution

Everything one input from the host causes: the messages and the timers that follow from it, through any number of actors, paid from one reservoir of fuel. A delivery or a tick from the host opens one, and every message it causes belongs to it.

In the code: an execution named by an xactor::correlation_id, whose fuel the scheduler’s fuel_of(correlation) tells.
See Fuel.

Fuel

The units an execution pays its work with: one for each message its actors send and each timer they arm, and the units they declare for their own work. An execution starts with the whole budget and is never refilled, so its work is bounded, whatever shape its messages take; what it cannot pay is refused, and changes nothing.

In the code: xactor::budgets::fuel, turn.fuel(), turn.spend(units) and errc::resource_limit.
See Fuel.

Guarantee

A rule xactor keeps whatever its caller does, under a number. A guarantee keeps its number and its anchor, and a comment of the code or of a test cites it by that anchor; the build of this page fails on a citation that names no anchor of it.

In the code: a comment of a test cites a guarantee by its anchor, the thirteenth as #xactor-invariant-13.
See Guarantees.

Host

The program around the actors: it creates them, delivers them messages, brings the time, runs a driver and reads what the actors ended with. A message from the host has no sender: its envelope’s from is the address 0.

In the code: the caller of create_actor and of the scheduler’s deliver, clock_tick, release_next, stop, status_of and output_of.
See Quick start.

Mailbox

Where an actor’s messages wait, in the order they were enqueued, until the actor has handled the one before: one first-in first-out queue for each actor, from which each turn of the actor takes one message.

In the code: a part of xactor::scheduler, which no logic sees.
See The scheduler.

Scheduler

The part of xactor that holds the actors and hands them their messages: one first-in first-out mailbox for each actor, and one first-in first-out queue of the actors that have messages waiting, each at most once. Each turn takes the actor at the front of the queue and hands it the first message of its mailbox, and the actor goes back to the end of the queue while its mailbox holds more. Delivering a message only puts it in a mailbox; a driver decides when turns happen.

In the code: xactor::scheduler<Message>, built with its budgets.
See The scheduler.

Static actor

An actor whose behaviour is a state machine compiled into the program: its states, events and transitions are C++ types and expressions, checked by the compiler and fixed when the program is built. An actor logic holds the machine as a member and hands each message to it, and the machine’s actions reach the system through the turn.

In the code: an actor logic over a Boost.MSM machine, whose events carry the message and the turn; xactor knows nothing of the library.
See Static actors.

Status

Whether an actor still receives messages: active, the one status in which it does, or done, error or stopped, each final.

In the code: xactor::status, which the scheduler’s status_of returns; its values are fixed and never reused.
See Lifecycle.

Timer

A message an actor arms for later, which comes back to the actor when the host’s time reaches its deadline. xactor reads no clock: the host brings the time.

In the code: turn.wake_at(deadline, message), with a key or without, turn.cancel_timer(key), and the scheduler’s clock_tick and release_next.
See Timers.

Turn

One delivery: the scheduler hands an actor one message, and the turn is everything the actor may do to the system while it handles that message. It sends and replies, spawns and stops its children, finishes or stops itself, arms and cancels timers and spends fuel, all through the turn, which knows the actor and the execution the message belongs to. A turn lives for one handle.

In the code: xactor::turn<Message>, handed to handle with the envelope.
See Turns.

Appendix B: Changelog

What changed in xactor in each version of webcpp, newest first, one line per change. "Unreleased" is what the next version brings. The release notes of every version are on webcpp’s index page.

Unreleased

webcpp 0.1.0

  • Breaking change: removed the guarantee that xactor runs without RTTI; build it with RTTI.

  • Changed the guarantee without exceptions to compiling only, which the lint checks.

  • Added the WEBCPP_XACTOR_NO_EXCEPTIONS configuration macro, defined automatically or by the developer.

  • Updated the documentation: a link to xstate, and a changelog appendix.

  • Changed config.hpp to include <webcpp/config.hpp>, webcpp’s configuration, which the build adds.

  • Added WEBCPP_NO_EXCEPTIONS, which disables xactor’s exceptions with every webcpp library’s.