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
resultand 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’sresultstill namesboost::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::variantof plain structs, the program’s own. Closed means every message is known when the program is compiled:std::visitover it checks that every alternative is handled, and the scheduler’s log records which alternative arrived by itsindex(). Every type of xactor that carries a message is a template over this variant, and xactor asks nothing more of it than to be astd::variantthat can be moved. A program may name the types it uses once, for instanceusing 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_actoror by a turn’sspawn_child, and named by theactor_refthey return. Anactor_refholds 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 |
|---|---|
|
The actor it is for. |
|
The actor that sent it; the address 0 when the host did, and the actor itself for the message of one of its timers. |
|
The execution it belongs to. |
|
Its number in the order messages entered a mailbox: the scheduler numbers each one as it enqueues it. |
|
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’sspawn_child<Logic>(inputs)pass their arguments to the constructor ofLogic. The quick start’s counter takes the echo’sactor_refas its input. -
Its state. Its members are the actor’s state, and only its own
handlechanges them: the counter’s sum and count of answers. -
One message at a time.
handleis called once for each message, and returns before the next is handed over. It reads the message fromcause.payload, withstd::get_if,std::holds_alternativeorstd::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
handlethat returns an error ends its actor with the statuserror, and the scheduler keeps the error forerror_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 anyboost::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 |
|---|---|
|
A null logic given to |
|
A |
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 |
|---|---|
|
A system whose executions each start with |
|
A free function: constructs a |
|
The same, for a logic already constructed, given as a |
|
Puts a message from the host in the mailbox of |
|
Hands exactly one message to its actor, and records it in |
|
Whether no message is waiting. |
|
|
|
What the actor finished with, or |
|
The error its handler returned, or an empty code unless it is |
|
The fuel an execution has left, and the budgets the system was built with (Fuel). |
|
Time and timers (Timers). |
|
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 |
|---|---|---|
|
Sends a message to an actor, itself included. |
1 |
|
Sends a message to the sender of the message being handled, in its execution. |
1 |
|
Constructs a |
0 |
|
Stops a child of this actor and every actor below it. Returns the actors it stopped (Lifecycle). |
0 |
|
Ends this actor with an output: it becomes |
0 |
|
Ends this actor as |
0 |
|
Arms a timer: the message comes back to this actor when the host’s time
reaches |
1 |
|
The same, replacing this actor’s timer under |
1 |
|
Removes this actor’s timer under |
0 |
|
The time the host brought last (Timers). |
0 |
|
Pays for work this actor does itself, all the units or none (Fuel). |
|
|
The fuel this execution has left. |
0 |
|
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
sendto an address that names no actor isinvalid_argument, and nothing is paid. Asendto 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
finishorstop. The actor receives nothing more, and in the rest of that turn it can still send, but awake_ator aspawn_childisinvalid_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,
sendandreplycost one unit,wake_at, with a key or without, one unit, andspend(n)n units, all from the execution of the message being handled.spawn_child,stop_child,stop,finish,cancel_timer,self,parent,nowandfuelcost nothing, and so does a delivery from the host. -
All or nothing. What the execution cannot pay fails with
resource_limitand changes nothing: no message is enqueued, no timer armed, no unit spent.spendtakes all its units or none, and a keyedwake_atthat is refused leaves the timer under its key as it was. -
Opening an execution. The first
deliver,clock_tickorrelease_nextof 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’sfuel_of(correlation)the same from outside; for an execution not yet opened,fuel_ofis 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_tickor arelease_nextthat 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 asturn.now() + delay. -
Released into the tick’s execution. A released timer’s message belongs to the execution the host gave
clock_tickorrelease_next, and releasing it costs nothing, since arming it was paid. Its envelope’sfromis the actor itself. -
Keys belong to their actor.
wake_atwith a key replaces this actor’s timer under that key, andcancel_timer(key)removes it, for free, and does nothing when there is none. Two actors may use the same key. A keyedwake_atthat the execution cannot pay leaves the timer under its key as it was. -
An ended actor has no timers. An actor that ends,
done,errororstopped, loses its timers, and in the rest of the turn that ended it, itswake_atisinvalid_argument, and nothing is paid. -
Time can move back. A tick earlier than the last moves
nowback: it releases what is due by then, and a deadline computed afterwards fromnowis 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 |
|---|---|---|---|
|
1 |
Every actor starts here: |
Its mailbox and its timers. |
|
2 |
Its turn called |
The output, for |
|
3 |
Its |
The error, for |
|
4 |
The host’s |
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 itdoneand keeps the output; a secondfinishisinvalid_argumentand changes nothing.output_ofisnullptrunless the actor isdone, anderror_ofis empty unless it iserror. -
An actor ends in one way. An error that
handlereturns 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)stopschildand 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 isinvalid_argumentfor anything but a child of the caller: the caller itself, a grandchild, an address that names no actor. The host’sstop(actor)does the same for any actor. -
Ending alone.
turn.stop()ends the actor handling the message asstopped, alone. An actor that finishes, fails or stops itself leaves its children running: they are its logic’s to stop, withstop_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_childandwake_atareinvalid_argument, andspawn_childconstructs 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 |
|---|---|---|
|
A program, native or WASI |
|
|
A test |
|
|
A native program that runs its turns as Asio handlers |
|
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 |
|---|---|
|
The message’s sequence number. |
|
Its receiver, and its sender, 0 for the host. |
|
Its execution. |
|
The fuel its execution had when it was delivered. |
|
Which alternative of the message variant it was, its |
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
handleleavesrun_oneand 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
handlereturns; 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
handlefails ends with the statuserror, 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 |
|---|---|
|
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. |
|
An actor’s statuses and how it ends: finishing, a failed |
|
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. |
|
|
|
The category |
|
|
|
Compile tests, which build |
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 |
wasip2 |
The toolset |
wasip3 |
The toolset |
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:
-
The messages of one mailbox are handled in the order they were enqueued.
-
run_onetakes 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. -
run_onehandles exactly one message, and returnsfalseonly when the scheduler is idle. A handler that returns an error ends its actor with the statuserrorand keeps the error forerror_of, unless its turn had already ended the actor, which then stays as it ended;run_onestill returnstrue, and every other actor keeps its turns. -
deliver,send,replyand 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). -
Each
correlation_idnames one execution. The firstdeliver,clock_tickorrelease_nextthat names it opens it withbudgets.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. -
Inside a turn,
sendandreplycost one unit,wake_at, with a key or without, one unit andspend(n)n units, all from the execution of the message being handled;spawn_child,stop_child,stop,finish,cancel_timer,self,parent,nowandfuelcost nothing. -
What the execution cannot pay fails with
resource_limitand changes nothing: nothing is enqueued, armed or spent, and a keyedwake_atleaves the timer under its key armed.spendtakes all its units or none. -
turn::fuelis the fuel the execution of the message being handled has left;scheduler::fuel_ofthe same from outside, and the whole budget for an execution not yet opened. -
replysends to the sender of the message being handled, in its execution. A message from the host has no sender, and a reply to it isinvalid_argument, as asendto an address that names no actor is; neither is paid. -
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.
-
No timer fires without a
clock_tickor arelease_nextthat reaches its deadline: time never advances by itself. -
A timer released by a
clock_tickor arelease_nextbelongs to that call’s execution, and its message’s sender is its own actor; releasing it costs nothing, since arming it was paid. -
run_until_idlereturns only whenidle()is true, andrun_turn(n)handles at most n messages. -
fifo_driverandasio_driverproduce identical envelope logs for the same deliveries. -
An envelope log given to
run_onerecords {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. -
Two schedulers built with the same budgets and fed the same deliveries produce identical envelope logs.
-
No turn blocks: nothing calls
io_context::run,run_oneorrun_for. -
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 asspawn,deliver,send,wake_ator the log’srecord, can still throwstd::bad_alloc, as a standard container does. -
There is no static mutable state: two schedulers in one process are independent.
-
test_driverexposesstep, exactly onerun_one, with or without a log, and nothing else; messages reach its scheduler throughdeliver. -
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
sendto an actor that has ended succeeds, and is paid. -
finish(output)makes the actor handling the message done and keeps the output foroutput_of. An actor ends once: afinishonce it has ended, a secondfinishor one afterstop(), fails withinvalid_argumentand changes nothing. -
stopstops an actor and its family asstop_childdoes (guarantee 31) and returns the actors it stopped; an ended one stays as it ended. -
output_ofisnullptrunless the status isdone;error_ofis empty unless the status iserror. -
The values of
statusnever change:active1,done2,error3,stopped4. -
The library names no message, id or error of any user. It asks of
Messageonly that it be astd::variantthat can be moved, whoseindex()the log records. -
create_actor<Logic>(system, inputs...)and a turn’sspawn_child<Logic>(inputs...)construct aLogicfrominputsand run it as a new, active actor of the same system, returning itsactor_ref;spawndoes the same for a logic already constructed, and refuses a null one withinvalid_argument. A turn of an actor that has ended refusesspawn_childbefore it constructs anything. -
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::parentreturns the parent, and nothing for an actor the host spawned.turn::selfis the actor handling the message. -
turn::stop_child(child)stopschildand 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 isinvalid_argumentfor 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. -
wake_at(deadline, message, key)replaces the same actor’s timer underkey;cancel_timer(key)removes it; keys belong to their actor. -
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_atandspawn_childareinvalid_argumentand nothing is paid.sendstill works. -
now()is the time the lastclock_tickorrelease_nextbrought, 0 before any;next_deadline()the earliest deadline armed, none while no timer is. A tick earlier than the last movesnowback: it releases what is due by then, and a deadline computed afterwards fromnowis earlier than one computed before it. -
turn::stop()ends the actor handling the message as stopped, alone: its children are its logic’s to stop, as afterfinish. It costs nothing, the rest of the turn runs as afterfinish, and astop()once the actor has ended, a second one or one afterfinish, isinvalid_argument. -
release_next(now, correlation)moves time tonowand releases the first timer due by then, asclock_tickwould 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 |
|---|---|
Defined when xactor is built without exceptions: when webcpp has none (the compiler has none, or the developer defined |
webcpp::xactor
Types
| Name | Description |
|---|---|
The behaviour of an actor, the base of the class a program writes it in. |
|
An actor’s address in its scheduler. |
|
The same turns, each posted as a handler onto one strand of an |
|
The limits a scheduler enforces, which it is constructed with. |
|
The execution every message caused by one host delivery belongs to; the execution’s fuel is kept under it. |
|
A message as the scheduler delivers it, and as |
|
The entries of the messages a scheduler delivered, in order. |
|
Delivers the queued messages in FIFO order, on the calling thread. |
|
One entry of an |
|
The system that holds the actors and gives each of them one message at a time, run by its host. |
|
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. |
|
One message per step, so a test observes exactly what one delivery changed. |
|
An actor’s turn: what it may do while it handles one message, the one delivery of the actor model. |
Type Aliases
| Name | Description |
|---|---|
What an operation that can fail returns: a |
Enums
| Name | Description |
|---|---|
The errors xactor reports, as |
|
Where an actor is in its life, which |
Functions
| Name | Description |
|---|---|
Runs a |
|
The category of every |
|
Builds a failed result of the type the call would have returned. |
|
Makes the |
|
Equality operators |
|
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; |
webcpp::xactor::actor_logic
The behaviour of an actor, the base of the class a program writes it in.
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 |
|---|---|
|
Constructors |
|
Destroys the logic; the scheduler that runs it does so as it is destroyed. |
Assignment operators |
|
|
Handles one message, the payload of |
Template Parameters
| Name | Description |
|---|---|
Message |
The type of every message of the system, the |
See Also
"The model", in the guide.
"Writing an actor logic", in the guide.
webcpp::xactor::actor_logic::actor_logic
Constructors
Synopses
Declared in <webcpp/xactor/actor_logic.hpp>
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.
webcpp::xactor::actor_logic::actor_logic
Not copyable: the scheduler owns a logic where it was constructed, and never moves it.
webcpp::xactor::actor_logic::actor_logic
Not movable: the scheduler owns a logic where it was constructed, and never moves it.
webcpp::xactor::actor_logic::~actor_logic
Destroys the logic; the scheduler that runs it does so as it is destroyed.
webcpp::xactor::actor_logic::operator=
Assignment operators
Synopses
Declared in <webcpp/xactor/actor_logic.hpp>
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.
actor_logic&
operator=(actor_logic&&) = delete;
webcpp::xactor::actor_logic::operator=
Not copy-assignable: the scheduler owns a logic where it was constructed, and never moves it.
Synopsis
Declared in <webcpp/xactor/actor_logic.hpp>
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
Declared in <webcpp/xactor/actor_logic.hpp>
actor_logic&
operator=(actor_logic&&) = delete;
webcpp::xactor::actor_logic::handle
Handles one message, the payload of cause.
Synopsis
Declared in <webcpp/xactor/actor_logic.hpp>
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.
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 |
|---|---|
The address: 1 for the first actor spawned, and 0 for none. |
Friends
| Name | Description |
|---|---|
Whether two addresses are the same. |
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.
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 |
Member Functions
| Name | Description |
|---|---|
|
Makes a driver of |
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.
Parameters
| Name | Description |
|---|---|
owner |
The scheduler whose turns it runs. |
Preconditions
-
owneroutlives 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
Declared in <webcpp/xactor/drivers.hpp>
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 |
webcpp::xactor::budgets
The limits a scheduler enforces, which it is constructed with.
Data Members
| Name | Description |
|---|---|
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.
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.
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 |
|---|---|
The number the host gave the execution. |
Friends
| Name | Description |
|---|---|
Orders two correlations by their values, so that one can key an ordered container. |
|
Whether two correlations name the same execution. |
webcpp::xactor::correlation_id::value
The number the host gave the execution.
webcpp::xactor::envelope
A message as the scheduler delivers it, and as actor_logic::handle receives it.
Description
The scheduler makes every envelope; a program reads them.
Data Members
| Name | Description |
|---|---|
The execution it belongs to, which pays for what its handling sends. |
|
The actor that sent it: address zero when the host sent it, and the actor itself for the message of one of its timers. |
|
The message. |
|
Its place in the order messages entered a mailbox, across every actor of the scheduler: 1 for the first. |
|
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.
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.
webcpp::xactor::envelope::sequence
Its place in the order messages entered a mailbox, across every actor of the scheduler: 1 for the first.
webcpp::xactor::envelope_log
The entries of the messages a scheduler delivered, in order.
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 |
|---|---|
The entries, in the order they were recorded. |
|
Appends the entry of a delivered message; |
Friends
| Name | Description |
|---|---|
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
Declared in <webcpp/xactor/envelope_log.hpp>
[[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
Declared in <webcpp/xactor/envelope_log.hpp>
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 |
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.
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 |
|---|---|
|
Makes a driver of |
Delivers at most |
|
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
Declared in <webcpp/xactor/drivers.hpp>
explicit
fifo_driver(scheduler<Message>& owner) noexcept;
Parameters
| Name | Description |
|---|---|
owner |
The scheduler whose turns it runs. |
Preconditions
-
owneroutlives 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
Declared in <webcpp/xactor/drivers.hpp>
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 |
webcpp::xactor::fifo_driver::run_until_idle
Delivers until nothing is left and returns how many were delivered.
Synopsis
Declared in <webcpp/xactor/drivers.hpp>
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 |
webcpp::xactor::logged_envelope
One entry of an envelope_log, a delivered envelope without its message.
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 |
Data Members
| Name | Description |
|---|---|
The execution it belonged to. |
|
The actor that sent it: address zero when the host did, and the actor itself for the message of one of its timers. |
|
Which alternative of the message variant it was, its |
|
The execution’s fuel when the message was delivered, before its actor handled it. |
|
The delivered envelope’s |
|
The actor it was delivered to. |
Friends
| Name | Description |
|---|---|
Whether two entries are equal, member by member. |
webcpp::xactor::logged_envelope::correlation
The execution it belonged to.
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.
webcpp::xactor::logged_envelope::payload_index
Which alternative of the message variant it was, its index().
webcpp::xactor::logged_envelope::remaining_fuel
The execution’s fuel when the message was delivered, before its actor handled it.
webcpp::xactor::logged_envelope::sequence
The delivered envelope’s envelope::sequence.
webcpp::xactor::logged_envelope::to
The actor it was delivered to.
webcpp::xactor::scheduler
The system that holds the actors and gives each of them one message at a time, run by its host.
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 |
|---|---|
|
Makes a system with no actor, at the time 0, whose executions each start with |
Moves time to |
|
Puts a message from the host in an actor’s mailbox; it runs when a driver delivers it. |
|
The error an actor’s handler returned, or an empty code. |
|
The fuel an execution has left; the whole budget for one not yet opened. |
|
Whether no message waits in any mailbox. |
|
The budgets the scheduler was constructed with. |
|
The earliest deadline of an armed timer; none while no timer is armed. |
|
The time the last |
|
The output an actor finished with, or |
|
How many timers are armed and not yet released. |
|
Moves time to |
|
Delivers exactly one message to its actor and returns whether there was one. |
|
Takes an actor into the system and gives it its address. |
|
Where an actor is in its life. |
|
Ends an actor from outside with its whole family, as |
Friends
| Name | Description |
|---|---|
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 |
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.
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
Declared in <webcpp/xactor/scheduler.hpp>
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
Declared in <webcpp/xactor/scheduler.hpp>
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, |
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
Declared in <webcpp/xactor/scheduler.hpp>
[[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
Declared in <webcpp/xactor/scheduler.hpp>
[[nodiscard]]
uint32_t
fuel_of(correlation_id correlation) const;
Parameters
| Name | Description |
|---|---|
correlation |
The execution. |
webcpp::xactor::scheduler::idle
Whether no message waits in any mailbox.
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
Declared in <webcpp/xactor/scheduler.hpp>
[[nodiscard]]
budgets const&
limits() const noexcept;
webcpp::xactor::scheduler::next_deadline
The earliest deadline of an armed timer; none while no timer is armed.
Synopsis
Declared in <webcpp/xactor/scheduler.hpp>
[[nodiscard]]
std::optional<uint64_t>
next_deadline() const noexcept;
webcpp::xactor::scheduler::now
The time the last clock_tick or release_next brought; 0 before any.
webcpp::xactor::scheduler::output_of
The output an actor finished with, or nullptr while it has none.
Synopsis
Declared in <webcpp/xactor/scheduler.hpp>
[[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
Declared in <webcpp/xactor/scheduler.hpp>
[[nodiscard]]
std::size_t
pending_timers() const noexcept;
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
Declared in <webcpp/xactor/scheduler.hpp>
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 |
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.
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 |
webcpp::xactor::scheduler::spawn
Takes an actor into the system and gives it its address.
Synopsis
Declared in <webcpp/xactor/scheduler.hpp>
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
Declared in <webcpp/xactor/scheduler.hpp>
[[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.
Synopsis
Declared in <webcpp/xactor/scheduler.hpp>
result<std::vector<actor_ref>>
stop(actor_ref address);
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.
Member Functions
| Name | Description |
|---|---|
The number after this one. |
Data Members
| Name | Description |
|---|---|
The place, counted from 1 for the first message enqueued. |
Friends
| Name | Description |
|---|---|
Orders two places, the earlier first. |
|
Whether two places are the same. |
webcpp::xactor::sequence_number::next
The number after this one.
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.
webcpp::xactor::test_driver
One message per step, so a test observes exactly what one delivery changed.
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 |
|---|---|
|
Makes a driver of |
|
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
Declared in <webcpp/xactor/drivers.hpp>
explicit
test_driver(scheduler<Message>& owner) noexcept;
Parameters
| Name | Description |
|---|---|
owner |
The scheduler whose turns it runs. |
Preconditions
-
owneroutlives the driver.
webcpp::xactor::test_driver::step
step overloads
Synopses
Declared in <webcpp/xactor/drivers.hpp>
Delivers one message, when one is waiting, and records nothing.
result<bool>
step();
Delivers one message, when one is waiting, and records it in log.
result<bool>
step(envelope_log& 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.
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.
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.
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 |
|---|---|
|
Makes the turn of the actor |
Removes this actor’s timer under |
|
Ends this actor with its output: it becomes done and receives nothing more. |
|
The fuel this execution has left. |
|
The time the last |
|
The actor whose turn spawned this one; none for one the host spawned. |
|
Sends a message to whoever sent the one being handled. |
|
This actor’s own reference. |
|
Sends a message to an actor, for one unit of fuel. |
|
Runs a |
|
Pays |
|
Ends this actor as stopped: nothing more reaches it, and its timers are dropped. |
|
Stops a child of this actor and every active actor descended from it. |
|
|
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
Declared in <webcpp/xactor/scheduler.hpp>
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
-
ownerandcauseoutlive the turn, andcause.tonames an actor ofowner.
webcpp::xactor::turn::cancel_timer
Removes this actor’s timer under key, if it has one.
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.
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.
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.
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
Declared in <webcpp/xactor/scheduler.hpp>
[[nodiscard]]
std::optional<actor_ref>
parent() const;
webcpp::xactor::turn::reply
Sends a message to whoever sent the one being handled.
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.
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
Declared in <webcpp/xactor/scheduler.hpp>
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
Declared in <webcpp/xactor/scheduler.hpp>
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 |
Inputs |
The types of the arguments a |
Parameters
| Name | Description |
|---|---|
inputs |
The arguments a |
webcpp::xactor::turn::spend
Pays units of fuel for work this actor does itself, all of them or none.
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.
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
Declared in <webcpp/xactor/scheduler.hpp>
result<std::vector<actor_ref>>
stop_child(actor_ref child);
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
Declared in <webcpp/xactor/scheduler.hpp>
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
-
Success;
errc::invalid_argument, and nothing paid, once this actor has ended;errc::resource_limitwhen the execution has no fuel left. -
Success;
errc::invalid_argument, and nothing paid, once this actor has ended;errc::resource_limitwhen the execution has no fuel left. A call refused leaves the timer underkeyarmed.
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
Declared in <webcpp/xactor/scheduler.hpp>
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
Declared in <webcpp/xactor/scheduler.hpp>
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.
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 |
Members
| Name | Description |
|---|---|
A call that names what it cannot act on, which changes nothing. |
|
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.
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.
webcpp::xactor::status
Where an actor is in its life, which scheduler::status_of returns.
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 |
|---|---|
Receives messages; every actor starts here. |
|
Finished with an output, through |
|
Its handler returned an error, which |
|
Stopped by |
See Also
"Lifecycle", in the guide.
webcpp::xactor::status::done
Finished with an output, through turn::finish, which scheduler::output_of returns.
webcpp::xactor::status::error
Its handler returned an error, which scheduler::error_of returns.
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.
webcpp::xactor::create_actor
Runs a Logic, constructed from inputs, as a new actor of system, and returns its reference.
Synopsis
Declared in <webcpp/xactor/scheduler.hpp>
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 |
Message |
The type of every message of the system, deduced from |
Inputs |
The types of the arguments a |
Parameters
| Name | Description |
|---|---|
system |
The scheduler that runs the actor. |
inputs |
The arguments a |
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>
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
Declared in <webcpp/xactor/envelope_log.hpp>
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
-
truewhen both name the same actor, or both name none. -
truewhen their values are equal. -
truewhen every member of one equals the same member of the other. -
truewhen 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>
|
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
Declared in <webcpp/xactor/envelope_log.hpp>
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
Declared in <webcpp/xactor/envelope_log.hpp>
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.
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, whichcreate_actor, the scheduler’sspawnand a turn’sspawn_childreturn.
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 ownhandlechanges them.In the code: a class derived from xactor’s
actor_logic<Message>, whosehandletakes the turn and the envelope and returns anxactor::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_driverfor a program,test_driverfor a test, one message perstep(), andasio_driverfor a native program that runs its turns as Boost.Asio handlers; each calls the scheduler’srun_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);fromis 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 torun_oneor to a driver, whoseentries()are each alogged_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’sfuel_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)anderrc::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
fromis the address 0.In the code: the caller of
create_actorand of the scheduler’sdeliver,clock_tick,release_next,stop,status_ofandoutput_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 itsbudgets.
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, ordone,errororstopped, each final.In the code:
xactor::status, which the scheduler’sstatus_ofreturns; 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’sclock_tickandrelease_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 tohandlewith 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_EXCEPTIONSconfiguration macro, defined automatically or by the developer. -
Updated the documentation: a link to xstate, and a changelog appendix.
-
Changed
config.hppto 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.