# sento documentation

## 1 Introduction

### Introduction - Actor framework featuring actors and agents

Sento is a 'message passing' library/framework with actors similar to Erlang or Akka. It supports creating systems that should work reactive, require parallel computing and event based message handling.

Sento features:

• Actors with ask (?) and tell (!) operations. ask can be asynchronous or synchronous.
• Agents: Agents are a specialization of Actors for wrapping state with a standardized interface of init, get and set. There are also specialized Agents for Common Lisps array and hash-map data structures.
• Router: Router offers a similar interface as Actor with ask and tell but can collects multiple Actors for load-balancing.
• EventStream: all Actors and Agents are connected to an EventStream and can subscribe to messages or publish messages. This is similar to an event-bus.
• Tasks: a simple API for concurrency.

### Intro

(Please also checkout the API documentation for further information) (for migrations from Sento v2, please check below migration guide)

#### Creating an actor-system

The first thing you wanna do is to create an actor system. In simple terms, an actor system is a container where all actors live in. So at any time the actor system knows which actors exist.

To create an actor system we can first change package to :sento-user because it imports the majority of necessary namespaces fopr convenience. Then, do:

(defvar *system* (make-actor-system))

When we look at *system* in the repl we see some information of the actor system:

#<ACTOR-SYSTEM config: (DISPATCHERS
(SHARED (WORKERS 4 STRATEGY RANDOM))
TIMEOUT-TIMER
(RESOLUTION 500 MAX-SIZE 1000)
EVENTSTREAM
(DISPATCHER-ID
SHARED)), user actors: 0, internal actors: 5>

The actor-system has, by default, four shared message dispatcher workers. Depending on how busy the system tends to be this default can be increased. Those four workers are part of the 'internal actors'. The 5th actor drives the event-stream (later more on that, but in a nutshell it's something like an event bus).

There are none 'user actors' yet, and the 'config' is the default config specifying the number of message dispatch workers (4) and the strategy they use to balance throughput, 'random' here.

Using a custom config is it possible to change much of those defaults. For instance, create custom dispatchers, i.e. a dedicated dispatcher used for the 'Tasks' api (see later for more info). The event-stream by default uses the global 'shared' dispatcher. Changing the config it would be possible to have the event-stream actor use a :pinned dispatcher (more on dispatchers later) to optimize throughput. Etc.

Actors live in the actor system, but more concrete in an actor-context. An actor-context contains a collection (of actors) and represents a Common Lisp protocol that defines a set of generic functions for creating, removing and finding actors in an actor-context. The actor system itself is also implementing the actor-context protocol, so it also acts as such and hence the protocol ac (actor-context) is used to operate on the actor system.

I.e. to shutdown the actor system one has to execute: (ac:shutdown *system*).

#### Creating and using actors

Now we want to create actors.

(actor-of *system* :name "answerer"
(lambda (msg)
(let ((output (format nil "Hello ~a" msg)))
(reply output))))

This creates an actor in *system*. Notice that the actor is not assigned to a variable (but you can). It is now registered in the system. Using function ac:find-actors you'll be able to find it again. Of course it makes sense to store important actors that are frequently used in a defparameter variable.

The :receive key argument to actor-of is a function which implements the message processing behaviour of an actor. The parameter to the 'receive' function is just the received message (msg).

actor-of also allows to specify the initial state, a name, and a custom actor type via key parameters. By default a standard actor of type 'actor is created. It is possible to subclass 'actor and specify your own. It is further possible to specify an 'after initialization' function, using the :init key, and 'after destroy' function using :destroy keyword. :init can, for example, be used to subscribe to the event-stream for listening to important messages.

The return value of 'receive' function is only used when using the synchronous ask-s function to 'ask' the actor. Using ask (equivalent: ?) the return value is ignored. If an answer should be provided to an asking actor, or if replying is part of an interface contract, then reply should be used.

The above actor was stored to a variable *answerer*. We can evaluate this in repl and see:

#<ACTOR path: /user/answerer, cell: #<ACTOR answerer, running: T, state: NIL, message-box: #<SENTO.MESSAGEB:MESSAGE-BOX/DP mesgb-1356, processed messages: 1, max-queue-size: 0, queue: #<SENTO.QUEUE:QUEUE-UNBOUNDED 82701A6D13>>>>

We'll see the 'path' of the actor. The prefix '/user' means that the actor was created in a user actor context of the actor system. Further we see whether the actor is 'running', its 'state' and the used 'message-box' type, by default it uses an unbounded queue.

Now, when sending a message using 'ask' pattern to the above actor like so:

(? *answerer* "FooBar")

we'll get a 'future' as result, because ?/ask is asynchronous.

#<FUTURE promise: #<BLACKBIRD-BASE:PROMISE
finished: NIL
errored: NIL
forward: NIL 80100E8B7B>>

We can check for a 'future' result. By now the answer from the *answerer* (via reply) should be available:

USER> (fresult *)
"Hello FooBar"

If the reply had not been received yet, fresult would return :not-ready. So, fresult doesn't block, it is necessary to repeatedly probe using fresult until result is other than :not-ready.

A nicer and asynchronous way without querying is to use fcompleted. Using fcompleted you setup a callback function that is called with the result when it is available. Like this:

(fcompleted
(result)
(format t "The answer is: ~a~%" result))

Which will asynchronously print "The answer is: Hello Buzz" after a short while. This will also work when the ask/? was used with a timeout, in which case result will be a tuple of (:handler-error . <ask-timeout condition>) if the operation timed out.

#### Creating child actors

To build actor hierarchies one has to create actors in actors. This is of course possible. There are two options for this.

1. Actors are created as part of actor-ofs :init function like so:
(actor-of *system*
(lambda (msg)
(let ((output (format nil "Hello ~a" msg)))
:init
(lambda (self)
(actor-of self
(lambda (msg)
(let ((output (format nil "~a" "Hello-child ~a" msg)))
(format nil "~a~%" output))))))

Notice the context for creating 'child-answerer', it is self, which is 'answerer-with-child'.

1. Or it is possible externally like so:
(actor-of *answerer* :name "child-answerer"
(lambda (msg)
(let ((output (format nil "~a" "Hello-child ~a" msg)))
(format nil "~a~%" output))))

This uses *answerer* context as parameter of actor-of. But has the same effect as above.

Now we can check if there is an actor in context of 'answerer-with-child':

USER> (all-actors *actor-with-child*)
(#<ACTOR path: /user/answerer-with-child/child-answerer, cell: #<ACTOR child-answerer, running: T, state: NIL, message-box: #<SENTO.MESSAGEB:MESSAGE-BOX/DP mesgb-1374, processed messages: 0, max-queue-size: 0, queue: #<SENTO.QUEUE:QUEUE-UNBOUNDED 8200A195FB>>>>)

#### Ping Pong

Another example that only works with tell/! (fire and forget).

We have those two actors.

The 'ping' actor:

(defparameter *ping*
(actor-of *system*
(lambda (msg)
(cond
((consp msg)
(case (car msg)
(:start-ping
(progn
(format t "Starting ping...~%")
(! (cdr msg) :ping *self*)))))
((eq msg :pong)
(progn
(format t "pong~%")
(sleep 2)
(reply :ping)))))))

And the 'pong' actor:

(defparameter *pong*
(actor-of *system*
(lambda (msg)
(case msg
(:ping
(progn
(format t "ping~%")
(sleep 2)
(reply :pong)))))))

The 'ping' actor understands a :start-ping message which is a cons and has as cdr the 'pong' actor instance. It also understands a :pong message as received from 'pong' actor.

The 'pong' actor only understands a :ping message. Each of the actors respond with either :ping or :pong respectively after waiting 2 seconds.

We trigger the ping-pong by doing:

(! *ping* (:start-ping . ,*pong*))

And then see in the console like:

Starting ping...
ping
pong
ping
...

To stop the ping-pong one just has to send (! *ping* :stop) to one of them.

:stop will completely stop the actors message processing, and the actor will not be useable anymore.

At last an example for the synchronous ask. ask-s is insofar similar top ask that it provides a result to the caller. However, it is not bound to reply as with ask. Here, the return value of the 'receive' function is returned to the caller, and ask-s will block until 'receive' function returns. Let's make an example:

(defparameter *s-asker*
(actor-of *sys*
(lambda (msg)
(cond
((stringp msg)
(format nil "Hello ~a" msg))
(t (format nil "Unknown message!"))))))

So we can do:

USER> (ask-s *s-asker* "Foo")
"Hello Foo"
"Unknown message!"

#### Dispatchers :pinned vs. :shared

Dispatchers are somewhat alike thread pools. Dispatchers of the :shared type are a pool of workers. Workers are actors using a :pinned dispatcher. :pinned just means that an actor spawns its own mailbox thread.

So :pinned and :shared are types of dispatchers. :pinned spawns its own mailbox thread, :shared uses a worker pool to handle the mailbox messages.

By default an actor created using actor-of uses a :shared dispatcher type which uses the shared message dispatcher that is automatically setup in the system.

When creating an actor it is possible to specify the dispatcher-id. This parameter specifies which 'dispatcher' should handle the mailbox queue/messages.

#### Finding actors in the context

If actors are not directly stored in a dynamic or lexical context they can still be looked up and used. The actor-context protocol contains a function find-actors which can lookup actors in various ways. Checkout the API documentation.

#### Mapping futures with fmap

Let's asume we have such a simple actor that just increments the value passed to it.

(defparameter *incer*
(actor-of *sys*
(reply (1+ value)))))

Since ask returns a future it is possible to map multiple ask operations like this:

(-> (ask *incer* 0)
(fmap (result)
(fmap (result)
(fcompleted (result)
(format t "result: ~a~%" result)
(assert (= result 3))))

A timeout (in seconds) can be specified for both ask-s and ask and is done like so:

To demonstrate this we could setup an example 'sleeper' actor:

(ac:actor-of *system*
(lambda (msg)
(sleep 5)))

If we store this to *sleeper* and do the following, the ask-s will return a handler-error with an ask-timeout condition.

(act:ask-s *sleeper* "Foo" :time-out 2)
(:HANDLER-ERROR . #<CL-GSERVER.UTILS:ASK-TIMEOUT #x30200319F97D>)

This works similar with the ask only that the future will be fulfilled with the handler-error cons.

To get a readable error message of the condition we can do:

CL-USER> (format t "~a" (cdr *))
A timeout set to 2 seconds occurred. Cause:
#<BORDEAUX-THREADS:TIMEOUT #x302002FAB73D> 

Note that ask-s uses the calling thread for the timeout checks.
ask uses a wheel timer to handle timeouts. The default resolution for ask timeouts is 500ms with a maximum size of wheel slots (registered timeouts) of 1000. What this means is that you can have timeouts of a multiple of 500ms and 1000 ask operations with timeouts. This default can be tweaked when creating an actor-system, see API documentation for more details.

#### Long running and asynchronous operations in receive

Be careful with doing long running computations in the receive function message handler, because it will block message processing. It is advised to use a third-party thread-pool or a library like lparallel to do the computations with, and return early from the receive message handler.

The computation result can be 'awaited' for in an asynchronous manner and a response to *sender* can be sent manually (via reply). The sender of the original message is set to the dynamic variable *sender*.

Due to an asynchronous callback of a computation running is a separate thread, the *sender* must be copied into a lexical environment because at the time of when the callback is executed the *sender* can have a different value.

For instance, if there is a potentially long running and asynchronous operation happening in 'receive', the original sender must be captured and the async operation executed in a lexical context, like so (receice function):

(lambda (msg)
(case msg
(:do-lengthy-op
(let ((sender *sender*))
;; do lengthy computation
(otherwise
;; do other non async stuff
(reply :my-reply))))

Notice that for the lengthy operation the sender must be captured because if the lengthy operation is asynchronous 'receive' function is perhaps called for another message where *sender* is different. In that case sender must be supplied explicitly for reply.

#### Changing behavior

An actor can change its behavior. The behavior is just a lambda similar as the 'receive' function taking the message as parameter.

The default behavior of the actor is given on actor construction using :receive key.

During the lifetime of an actor the behavior can be changed using become. unbecome will restore the default behavior.

Here is an example:

(ac:actor-of *system*
(lambda (msg)
(case msg
(:open
(progn
(unstash-all)
(become (lambda (msg)
(case msg
(:write
;; do something
)
(:close
(unbecome))
(otherwise
(stash msg)))))))
(otherwise (stash msg)))))

#### Stashing messages

Stashing allows the actor to stash away messages for when the actor is in a state that doesn't allow it to handle certain messages. unstash-all can unstash all stashed messages.

#### Creating actors without a system

It is still possible to create actors without a system. This is how you do it:

;; make an actor
(defvar *my-actor* (act:make-actor (lambda (msg)
(format t "FooBar"))
:name "Lone-actor"))
;; setup a thread based message box
(setf (act-cell:msgbox *my-actor*)
(make-instance 'mesgb:message-box/bt))

You have to take care yourself about stopping the actor and freeing resources.

### Agents

An Agent is a specialized Actor. It is meant primarily for maintaining state and comes with some conveniences to do that.

To use an Agent import sento.agent package.

There is no need to subclass an Agent. Rather create a facade to customize an agent. See below.

An Agent provides three functions to use it.

• make-agent creates a new agent. Optionally specify an actor-context or define the kind of dispatcher the agent should use.
• agent-get retrieves the current state of the agent. This directly delivers the state of the agent for performance reasons. There is no message handling involved.
• agent-update updates the state of the agent
• agent-update-and-get updates the agent state and returns the new state.

All four take a lambda. The lambda for make-agent does not take a parameter. It should return the initial state of the agent. agent-get and agent-update both take a lambda that must support one parameter. This parameter represents the current state of the agent.

Let's make a simple example:

First create an agent with an initial state of 0.

(defparameter *my-agent* (make-agent (lambda () 0)))

Now update the state several times (agent-update is asynchronous and returns t immediately):

(agent-update *my-agent* (lambda (state) (1+ state)))

Finally get the state:

(agent-get *my-agent* #'identity)

This agent-get just uses the identity function to return the state as is.

So this simple agent represents a counter.

It is important to note that the retrieves state, i.e. with identity should not be modified outside the agent.

#### Using an agent within an actor-system

The make-agent constructor function allows to provide an optional actor-context argument that, when given, makes the constructor create the agent within the given actor-context. Another parameter dispatcher-id allows to specify the dispatcher where :shared is the default, :pinned will create the agent with a separate mailbox thread.

It also implies that the agent is destroyed then the actor-system is destroyed.

However, while actors can create hierarchies, agents can not. Also the API for creating agents in systems is different to actors. This is to make explicit that agents are treated slightly differently than actors even though under the hood agents are actors.

#### Wrapping an agent

While you can use the agent as in the example above it is usually advised to wrap an agent behind a more simple facade that doesn't work with lambdas and allows a more domain specific naming.

For example could a facade for the counter above look like this:

(defvar *counter-agent* nil)

(defun init-agent (initial-value)
(setf *counter-agent* (make-agent (lambda () initial-value))))

(defun increment () (agent-update *counter-agent* #'1+))
(defun decrement () (agent-update *counter-agent* #'1-))
(defun counter-value () (agent-get *counter-agent* #'identity))

Alternatively, one can wrap an agent inside a class and provide methods for simplified access to it.

### Router

A Router is a facade over a set of actors. Routers are either created with a set of actors using the default constructor router:make-router or actors can be added later.

Routers implement part of the actor protocol, so it allows to use tell, ask-s or ask which it forwards to a 'routee' (one of the actors of a router) by passing all of the given parameters. The routee is chosen by applying a strategy. The built-in default strategy a routee is chosen randomly.

The strategy can be configured when creating a router using the constructors &key parameter :strategy. The strategy is just a function that takes the number of routees and returns a routee index to be chosen for the next operation.

Currently available strategies: :random and:round-robin.

Custom strategies can be implemented.

### Dispatchers

#### :shared

A :shared dispatcher is a facility that is set up in the actor-system. It consists of a configurable pool of 'dispatcher workers' (which are in fact actors). Those dispatcher workers execute the message handling in behalf of the actor and with the actors message handling code. This is protected by a lock so that ever only one dispatcher will run code on an actor. This is to ensure protection from data race conditions of the state data of the actor (or other slots of the actor).

Using this dispatcher allows to create a large number of actors. The actors as such are generally very cheap.

#### :pinned

The :pinned dispatcher is represented by just a thread that operates on the actors message queue. It handles one message after another with the actors message handling code. This also ensures protection from data race conditions of the state of the actor.

This variant is slightly faster (see below) but requires one thread per actor.

#### custom dispatcher

It is possible to create additional dispatcher of type :shared. A name can be freely chosen, but by convention it should be a global symbol, i.e. :my-dispatcher.

When creating actors using act:actor-of, or when using the tasks API it is possible to specify the dispatcher (via the 'dispatcher-id' i.e. :my-dispatcher) that should handle the actor, agent, or task messages.

A custom dispatcher is in particular useful when using tasks for longer running operations. Longer running operations should not be used for the :shared dispatcher because it is, by default, responsible for the message handling of most actors.

### Eventstream

The eventstream allows messages (or events) to be posted on the eventstream in a fire-and-forget kind of way. Actors can subscribe to the eventstream if they want to get notified for particular messages or any message posted to the event stream.
This allows to create event-based systems.

Here is a simple example:

(defparameter *sys* (asys:make-actor-system))

(ac:actor-of *sys* :name "listener"
:init (lambda (self)
(ev:subscribe self self 'string))
(cond
((string= "my-message" msg)
(format t "received event: ~a~%" msg)))))

(ev:publish *sys* "my-message")

This subscribes to all 'string based events and just prints the message when received.
The subscription here is done using the :init hook of the actor. The ev:subscribe function requires to specify the eventstream as first argument. But there are different variants of the generic function defined which allows to specify an actor directly. The eventstream is retrieve from the actor through its actor-context.

received event: my-message

See the API documentation for more details.

'tasks' is a convenience package that makes dealing with asynchronous and concurrent operations very easy.

Here is a simple example:

(defparameter *sys* (make-actor-system))

(with-context (*sys*)

// run something without requiring a feedback

// run asynchronous - with await
// do some other stuff
// eventually we need the task result

// run asynchronous with completion-handler (continuation)
:on-complete-fun
(lambda (result)
(do-something-with result)))

// concurrently map over the given list
(->>
'(1 2 3 4 5)
(reduce #'+)))

=> 20 (5 bits, #x14, #o24, #b10100)


All functions available in 'tasks' package require to be wrapped in a with-context macro. This macro removes the necessity of an additional argument to each of the functions which is instead supplied by the macro.

What happens in the example above is that the list '(1 2 3 4 5) is passed to task-async-stream. task-async-stream then spawns a 'task' for each element of the list and applies the given function (here 1+) on each list element. The function though is executed by a worker of the actor-systems :shared dispatcher. task-async-stream then also collects the result of all workers. In the last step (reduce) the sum of the elements of the result list are calculated.

It is possible to specify a second argument to the with-context macro to specify the dispatcher that should be used for the tasks.
The concurrency here depends on the number of dispatcher workers.

As alternative, or in special circumstances, it is possible to setf *task-context* and/or *task-dispatcher* special variables which allows to use tasks without with-context macro.

Be also aware that the :shared dispatcher should not run long running operations as it blocks a message processing thread. Create a custom dispatcher to use for tasks when you plan to operate longer running operations.

See the API documentation for more details.

### Immutability

Some words on immutability. Actor states don't need to be immutable data structures. Sento does not make copies of the actor states. The user is responsible for the actor state and to motate the actor state only within 'receive' function.

### Logging

Sento does its own logging using different log levels from 'trace' to 'error' using log4cl. If you wish to also use log4cl in your application but find that Sento is too noisy in debug and trace logging you can change the log level for the 'sento package only by:

(log:config '(sento) :warn)

This will tell log4cl to do any logging for sento in warn level.

### Benchmarks

Hardware specs (M1)):

• Mac M1 Ultra, 32 GB RAM

Hardware specs (x86-64):

• iMac Pro (2017), 8 Core Xeon, 32 GB RAM

All

Version 3 of Sento uses the jpl-queues package which is slightly slower than the lparallel cons-queue. The lparallel cons-queue package is available as separate asdf system if needed and if the additional dependency is acceptable.

The benchmark was created by having 8 threads throwing each 125k (1m altogether) messages at 1 actor. The timing was taken for when the actor did finish processing those 1m messages. The messages were sent by either all tell, ask-s, or ask to an actor whose message-box worked using a single thread (:pinned) or a dispatched message queue (:shared / dispatched) with 8 workers.

Of course a tell is in most cases the fastest one, because it's the least resource intensive and there is no place that is blocking in this workflow.

SBCL (v2.3.0)

Even though SBCL is by far the fastest one with tell on both :pinned and dispatched, it had massive problems on dispatched - ask-s where I had to lower the number of messages to 200k alltogether. Beyond that value SBCL didn't get it worked out.

LispWorks (8.0.1)

LispWorks is fast overall. Not as fast as SBCL. But it seems the GC is more robust, in particular on the dispatched - ask.

CCL (v1.12)

Unfortunately CCL doesn't work natively on M1 Apple CPU.

ABCL (1.9)

The pleasant surprise was ABCL. While not being the fastest it is the most robust. Where SBCL and CCL were struggling you could throw anything at ABCL and it'll cope with it. I'm assuming that this is because of the massively battle proven Java Runtime.

### Migration guide for moving from Sento 2 to Sento 3

• the receive function is now 1-arity. It only takes the a message parameter. Previous 'self' and 'state' parameters are now accessible via *self* and *state*. The same applies to become function.

• the return value of 'receive' function is ignored for tell and ask. In both cases a reply macro can be used to reply to a sender. reply implicitly uses *sender* but can be overriden (see 'long running and asynchronous operations in receive'). The 'receive' function return value is still relevant for ask-s.

• the lparallel dependency was removed to reduce dependencies. However, the cons-queue of lparallel is very fast (used for unbounded message queue) so an additional 'sento-high-speed-queue' asdf system has been added to bring back the lparallel cons-queue if performance is critical. It brings an additional 10%-30% boost.

• 'utils' package has been split to 'timeutils' for i.e. ask-timeout condition, and 'miscutils' for i.e. filter function.

### Version history

Version 3.0.0 (1.2.2023): New major version. See migration guide if you have are migrating from version 2.

Version 2.2.0 (27.12.2022): Added stashing and unstashing of messages.

Version 2.1.0 (17.11.2022): Reworked the future package. Nicer syntax and futures can now be mapped.

Version 2.0.0 (16.8.2022): Rename to "Sento". Incompatible change due to package names and system have changed.

Version 1.12.2 (29.5.2022): Removed the logging abstraction again. Less code to maintain. log4cl is featureful enough for users to either use it, or use something else in the applications that are based on sento.

Version 1.12.1 (25.5.2022): Shutdown and stop of actor, actor context and actor system can now wait for a full shutdown/stop of all actors to really have a clean system shutdown.

Version 1.12.0 (26.2.2022): Refactored and cleaned up the available actor-of facilities. There is now only one. If you used the macro before, you may have to adapt slightly.

Version 1.11.1 (25.2.2022): Minor additions to actor-of macro to allow specifying a destroy function.

Version 1.11.0 (16.1.2022): Changes to AC:FIND-ACTORS. Breaking API change. See API documentation for details.

Version 1.10.0: Logging abstraction. Use your own logging facility. sento doesn't lock you in but provides support for log4cl. Support for other logging facilities can be easily added so that the logging of sento will use your chosen logging library. See below for more details.

Version 1.9.0: Use wheel timer for ask timeouts.

Version 1.8.2: atomic add/remove of actors in actor-context.

Version 1.8.0: hash-agent interface changes. Added array-agent.

Version 1.7.6: Added cl:hash-table based agent with similar API interface.

Version 1.7.5: Allow agent to specify the dispatcher to be used.

Version 1.7.3: cleaned up dependencies. Now sento works on SBCL, CCL, LispWorks, Allegro and ABCL

Version 1.7.2: allowing to choose the dispatcher strategy via configuration

Version 1.7.1: added possibility to create additional and custom dispatchers. I.e. to be used with tasks.

Version 1.7.0: added tasks abstraction facility to more easily deal with asynchronous and concurrent operations.

Version 1.6.0: added eventstream facility for building event based systems. Plus documentation improvements.

Version 1.5.0: added configuration structure. actor-system can now be created with a configuration. More configuration options to come.

Version 1.4.1: changed documentation to the excellent mgl-pax

Version 1.4: convenience macro for creating actor. See below for more details

Version 1.3.1: round-robin strategy for router

Version 1.3: agents can be created in actor-system

Version 1.2: introduces a breaking change

ask has been renamed to ask-s.

async-ask has been renamed to ask.

The proposed default way to query for a result from another actor should be an asynchronous ask. ask-s (synchronous) is of course still possible.

Version 1.0 of sento library comes with quite a few new features (compared to the previous 0.x versions). One of the major new features is that an actor is not bound to it's own message dispatcher thread. Instead, when an actor-system is set-up, actors can use a shared pool of message dispatchers which effectively allows to create millions of actors.

It is now possible to create actor hierarchies. An actor can have child actors. An actor now can also 'watch' another actor to get notified about it's termination.

It is also possible to specify timeouts for the ask-s and ask functionality.

This new version is closer to Akka (the actor model framework on the JVM) than to GenServer on Erlang. This is because Common Lisp from a runtime perspective is closer to JVM than to Erlang/OTP. Threads in Common Lisp are heavy weight OS threads rather than user-space low weight 'Erlang' threads (I'd like to avoid 'green threads', because threads in Erlang are not really green threads). While on Erlang it is easily possible to spawn millions of processes/threads and so each actor (GenServer) has its own process, this model is not possible when the threads are OS threads, because of OS resource limits. This is the main reason for working with the message dispatcher pool instead.

## 2 API documentation

### 2.1 Actor-System

###### [in package SENTO.ACTOR-SYSTEM with nicknames ASYS]

• [class] ACTOR-SYSTEM

An actor-system is the opening facility. The first thing you do is to create an actor-system using the main constructor make-actor-system. With the actor-system you can create actors via the ac:actor-context protocol function: ac:actor-of.

Or even simpler via act:actor-of which is a convenience macro:

(act:actor-of (*system*)
(lambda (msg)
;; do stuff))

• [variable] *DEFAULT-CONFIG* (:DISPATCHERS (:SHARED (:WORKERS 4 :STRATEGY :RANDOM)) :TIMEOUT-TIMER (:RESOLUTION 500 :MAX-SIZE 1000) :EVENTSTREAM (:DISPATCHER-ID :SHARED))

The default config used when creating an asys:actor-system. The actor-system constructor allows to provide custom config options that override the default.

• [function] REGISTER-DISPATCHER SYSTEM DISPATCHER

Registers a dispatcher to the actor-system.

• system: the actor-system

• dispatcher: the dispatcher instance.

• [function] REGISTER-NEW-DISPATCHER SYSTEM DISPATCHER-ID &KEY WORKERS STRATEGY

Makes and registers a new dispatcher.

• system: the actor-system

• dispatcher-id: the dispatcher identifier. Usually a global symbol like :foo

• :workers: key argument for the number of workers.

• :strategy: key argument for the dispatcher strategy (:random or :round-robin)

• [method] ACTOR-OF (SYSTEM ACTOR-SYSTEM)

See ac:actor-of

• [method] FIND-ACTORS (SELF ACTOR-SYSTEM) PATH

See ac:find-actors

• [method] ALL-ACTORS (SELF ACTOR-SYSTEM)

See ac:all-actors

• [method] STOP (SELF ACTOR-SYSTEM) ACTOR

See ac:stop

• [method] SHUTDOWN (SELF ACTOR-SYSTEM)

See ac:shutdown

### 2.2 Actor-Context

###### [in package SENTO.ACTOR-CONTEXT with nicknames AC]

• [class] ACTOR-CONTEXT

actor-context deals with creating and maintaining actors. The actor-system and the actor itself are composed of an actor-context.

• [method] ACTOR-OF (CONTEXT ACTOR-CONTEXT)

See ac:actor-of.

• [method] FIND-ACTORS (CONTEXT ACTOR-CONTEXT) PATH

See ac:find-actors

• [method] ALL-ACTORS (CONTEXT ACTOR-CONTEXT)

See ac:all-actors

• [method] STOP (CONTEXT ACTOR-CONTEXT) ACTOR

See ac:stop

• [method] SHUTDOWN (CONTEXT ACTOR-CONTEXT)

See ac:shutdown

• [generic-function] NOTIFY CONTEXT ACTOR NOTIFICATION

Notify the actor-context about something that happened to an actor. Current exists:

• :stopped: this will remove the actor from the context.

• [reader] SYSTEM ACTOR-CONTEXT (= NIL)

A reference to the actor-system.

• [reader] ID ACTOR-CONTEXT (:ID = NIL)

The id of this actor-context. Usually a string.

#### 2.2.1 Actor-Context protocol

• [generic-function] ACTOR-OF CONTEXT &KEY RECEIVE INIT DESTROY DISPATCHER STATE TYPE NAME

Interface for creating an actor.

!!! Attention: this factory function wraps the act:make-actor functionality to something more simple to use. Using this function there is no need to use both act:make-actor.

context is either an asys:actor-system, an ac:actor-context, or an act:actor (any type of actor). The new actor is created in the given context.

• :receive is required and must be a 3-arity lambda with arguments: 1. the actor, 2. the message, 3. the state Usually expressed as (lambda (self msg state)).

• :init: is an optional initialization function with one argument: the actor instance (self). This represents a 'start' hook that is called after the actor was fully initialized.

• :destroy: is an optional destroy function also with the actor instance as argument. This function allows to unsubsribe from event-stream or such.

• :state key can be used to initialize with a state.

• :dispatcher key can be used to define the message dispatcher manually. Options are :shared (default) and :pinned.

• :type can specify a custom actor class. See act:make-actor for more info.

• :name to set a specific name to the actor, otherwise a random name will be used.

• [generic-function] FIND-ACTORS CONTEXT PATH &KEY TEST KEY

Returns actors to be found by the criteria of:

• context: an AC:ACTOR-CONTEXT, or an ACT:ACTOR or an ASYS:ACTOR-SYSTEM as all three implement find-actors.

• path: a path designator to be found. This can be just an actor name, like 'foo', then find-actors will only look in the given context for the actor. It can also be: 'foo/bar', a relative path, in which case find-actors will traverse the path (here 'bar' is a child of 'foo') to the last context and will try to find the actor by name there, 'bar' in this case. Also possible is a root path like '/user/foo/bar' which will start traversing contexts started from the root context, which is the actor-system.

• test: a 2-arity test function where the 1st argument is the path, the 2nd is the a result of the key function (which defaults to ACT-CELL:NAME, so the name of the actor). The default function for test is STRING=. However, in case of a multi-subpath path both test and key only apply to the last path component, which designates the actor name to be found.

• key: a 1-arity function applied on an actor instance. Defaults to ACT-CELL:NAME.

Depending on test function the last path component can be used as a wildcard when using a test function like STR:STARTS-WITH-P or STR:CONTAINSP for example.

• [generic-function] ALL-ACTORS CONTEXT

Retrieves all actors of this context as a list

• [generic-function] STOP CONTEXT ACTOR &KEY WAIT

Stops the given actor on the context. The context may either be an actor-context, or an actor-system. The actor is then also removed from the context. Specify wait as T to block until the actor is stopped (default NIL).

• [generic-function] SHUTDOWN CONTEXT &KEY WAIT

Stops all actors in this context. When the context is an actor-context this still stop the actor context and all its actors. For the actor-system it will stop the whole system with all actors. Specify wait as T to block until all actors of the context are stopped (default NIL).

### 2.3 Actor

###### [in package SENTO.ACTOR with nicknames ACT]

• [class] ACTOR ACTOR-CELL

This is the actor class.

The actor does its message handling using the receive function.

The receive function takes one argument (the message). For backwards compatibility and for convenience it can still be used to provide an immediate return for act:ask-s. act:tell and act:ask ignore a return value.

There is asynchronous tell, a synchronous ask-s and asynchronous ask which all can be used to send messages to the actor. ask-s provides a synchronous return taken from the receive functions return value. 'ask' provides a return wrapped in a future. But the actor has to explicitly use *sender* to formulate a response. tell is just fire and forget.

To stop an actors message processing in order to cleanup resouces you should tell (or ask-s) the :stop message. It will respond with :stopped (in case of ask(-s)).

• [generic-function] MAKE-ACTOR RECEIVE &KEY NAME STATE TYPE INIT DESTROY

Constructs an actor.

Arguments:

• receive: message handling function taking one argument, the message.

• name: give the actor a name. Must be unique within an ac:actor-context.

• type: Specify a custom actor class as the :type key. Defaults to 'actor. Say you have a custom actor custom-actor and want make-actor create an instance of it. Then specify :type 'custom-actor on make-actor function. If you have additional initializations to make you can do so in initialize-instance.

• state: initialize an actor with a state. (default is nil)

• init and destroy: are functions that take one argument, the actor instance. Those hooks are called on (after) initialization and (after) stop respectively.

• [generic-function] TELL ACTOR MESSAGE &OPTIONAL SENDER

Sends a message to the actor. tell is asynchronous. tell does not expect a result. If a sender is specified the receiver will be able to send a response.

Alternatively to the tell function one can equally use the ! function designator.

• [generic-function] ASK-S ACTOR MESSAGE &KEY TIME-OUT

Sends a message to the actor. ask-s is synchronous and waits for a result. Specify timeout if a message is to be expected after a certain time. An :handler-error with timeout condition will be returned if the call timed out.

ask-s assumes, no matter if ask-s is issued from outside or inside an actor, that the response is delivered back to the caller. That's why ask-s does block the execution until the result is available. The receive function return value will be used as the result of receive.

• [generic-function] ASK ACTOR MESSAGE &KEY TIME-OUT

Sends a message to the actor. A future is returned. Specify timeout if a message is to be expected after a certain time. An :handler-error with timeout condition will be returned is the call timed out.

An ask is similar to a ask-s in that the caller gets back a result but it doesn't have to actively wait for it. Instead a future wraps the result. However, the internal message handling is based on tell. How this works is that the message to the target actor is not 'sent' using the callers thread but instead an anonymous actor is started behind the scenes. This anonymous actor can weit for a response from the target actor. The response then fulfills the future.

Alternatively to the ask function one can equally use the ? function designator.

• [function] REPLY MSG &OPTIONAL (SENDER *SENDER*)

Replies to a sender. Sender must exist. Use this from within receive function to reply to a sender.

• [generic-function] BECOME NEW-BEHAVIOR

Changes the receive of the actor to the given new-behavior function. The new-behavior function must accept 3 parameters: the actor instance, the message and the current state. This function should be called from within the behavior receive function.

• [generic-function] UNBECOME

Reverts any behavior applied via become back to the default receive function. This function should be called from within the behavior receive function.

• [generic-function] CONTEXT ACTOR

This is the actor-context every actor is composed of. When the actor is created from scratch it has no actor-context. When created through the actor-contexts, or system's actor-of function an actor-context will be set.

• [generic-function] PATH ACTOR

The path of the actor, including the actor itself. The path denotes a tree which starts at the system context.

• [generic-function] WATCH ACTOR WATCHER

Registers watcher as a watcher of actor. Watching lets the watcher know about lifecycle changes of the actor being watched. I.e.: when it stopped. The message being sent in this case is: (cons :stopped actor-instance)

• [generic-function] UNWATCH ACTOR WATCHER

Unregisters watcher of actor.

• [generic-function] WATCHERS ACTOR

Returns a list of watchers of actor.

#### 2.3.1 Actor-Cell

###### [in package SENTO.ACTOR-CELL with nicknames ACT-CELL]

• [class] ACTOR-CELL

actor-cell is the base of the actor. It encapsulates state and can executes async operations. State can be changed by setfing *state* special variable from inside receive function, via calling call or cast. Where call is waiting for a result and cast does not. For each call and cast handlers must be implemented by subclasses.

It uses a message-box to processes the received messages. When the actor/actor-cell was created ad-hoc (out of the actor-system/actor-context), it will not have a message-box and can't process messages. When the actor is created through the actor-system or actor-context, one can decide what kind of message-box/dispatcher should be used for the new actor.

See actor-context actor-of method for more information on this.

To stop an actor message handling and you can send the :stop message either via call (which will respond with :stopped) or cast. This is to cleanup thread resources when the actor is not needed anymore.

Note: the actor-cell uses call and cast functions which translate to ask-s and tell on the actor.

• [reader] NAME ACTOR-CELL (:NAME = (STRING (GENSYM "actor-")))

The name of the actor/actor-cell. If no name is specified a default one is applied.

• [reader] STATE ACTOR-CELL (:STATE = NIL)

The encapsulated state.

• [accessor] MSGBOX ACTOR-CELL (= NIL)

The message-box. By default the actor/actor-cell has no message-box. When the actor is created through the actor-context of an actor, or the actor-system then it will be populated with a message-box.

• [generic-function] HANDLE-CALL ACTOR-CELL MESSAGE

Handles calls to the server. Must be implemented by subclasses. The result of the last expression of this function is returned back to the 'caller'. State of the cell can be changed via setfing *state* variable.

• [generic-function] HANDLE-CAST ACTOR-CELL MESSAGE

Handles casts to the server. Must be implemented by subclasses. State of the cell can be changed via setfing *state* variable.

• [generic-function] AFTER-STOP ACTOR-CELL

Generic function definition that is called after the actor cell has stopped.

• [generic-function] STOP ACTOR-CELL &OPTIONAL WAIT

Stops the actor-cells message processing gracefully. This is not an immediate stop.
There are two ways to stop an actor (cell).

1. by calling this function. It is not an immediate stop. The actor will finish the current message processing.
wait: waits until the cell is stopped.

2. by sending :stop to the actor (cell). This won't allow to wait when the actor is stopped, even not with ask-s. The :stop message (symbol) is normally processed by the actors message processing.

• [function] CALL ACTOR-CELL MESSAGE &KEY (TIME-OUT NIL)

Send a message to a actor-cell instance and wait for a result. Specify a timeout in seconds if you require a result within a certain period of time. Be aware though that this is a resource intensive wait based on a waiting thread. The result can be of different types. Normal result: the last expression of handle-call (or receive in act:actor) implementation. Error result: (cons :handler-error )' In case of time-out the error condition is a bt:timeout.

• [function] CAST ACTOR-CELL MESSAGE &OPTIONAL SENDER

Sends a message to a actor-cell asynchronously. There is no result. If a sender' is specified the result will be sent to the sender.

• [function] RUNNING-P ACTOR-CELL

Returns true if this server is running. nil otherwise.

#### 2.3.2 Message-box base class

###### [in package SENTO.MESSAGEB with nicknames MESGB]

• [class] MESSAGE-BOX-BASE

The user does not need to create a message-box manually. It is automatically created and added to the actor when the actor is created through act:actor-of or ac:actor-of.

• [reader] NAME MESSAGE-BOX-BASE (:NAME = (STRING (GENSYM "mesgb-")))

The name of the message-box. The default name is concatenated of "mesgb-" and a gensym generated random number.

• [reader] MAX-QUEUE-SIZE MESSAGE-BOX-BASE (:MAX-QUEUE-SIZE = 0)

0 or nil will make an unbounded queue. A value > 0 will make a bounded queue. Don't make it too small. A queue size of 1000 might be a good choice.

• [generic-function] SUBMIT MESSAGE-BOX-BASE MESSAGE WITHREPLY-P TIME-OUT HANDLER-FUN

Submit a message to the mailbox to be queued and handled.

• [generic-function] STOP MESSAGE-BOX-BASE &OPTIONAL WAIT

Stops the message processing. The message processing is not terminated while a message is still processed. Rather it is a graceful stop by waiting until a message has been processed. Provide wait EQ T to wait until the actor cell is stopped.

• [method] STOP (SELF MESSAGE-BOX-BASE)

###### [in package SENTO.MESSAGEB with nicknames MESGB]

• [class] MESSAGE-BOX/BT MESSAGE-BOX-BASE

Bordeaux-Threads based message-box with a single thread operating on a message queue. This is used when the actor is created using a :pinned dispatcher type. There is a limit on the maximum number of actors/agents that can be created with this kind of queue because each message-box (and with that each actor) requires exactly one thread.

• [method] SUBMIT (SELF MESSAGE-BOX/BT) MESSAGE WITHREPLY-P TIME-OUT HANDLER-FUN

Alternatively use with-submit-handler from your code to handle the message after it was 'popped' from the queue. The handler-fun argument here will be funcalled when the message was 'popped'.

#### 2.3.4 Message-box dispatched

###### [in package SENTO.MESSAGEB with nicknames MESGB]

• [class] MESSAGE-BOX/DP MESSAGE-BOX-BASE

This message box is a message-box that uses the systems dispatcher. This has the advantage that an almost unlimited actors/agents can be created. This message-box doesn't 'own' a thread. It uses the dispatcher to handle the message processing. The dispatcher is kind of like a thread pool.

• [method] SUBMIT (SELF MESSAGE-BOX/DP) MESSAGE WITHREPLY-P TIME-OUT HANDLER-FUN

Submitting a message on a multi-threaded dispatcher is different as submitting on a single threaded message-box. On a single threaded message-box the order of message processing is guaranteed even when submitting from multiple threads. On the dispatcher this is not the case. The order cannot be guaranteed when messages are processed by different dispatcher threads. However, we still guarantee a 'single-threadedness' regarding the state of the actor. This is achieved here by protecting the handler-fun execution with a lock.

The time-out with the 'dispatcher mailbox' assumes that the message received the dispatcher queue and the handler in a reasonable amount of time, so that the effective time-out applies on the actual handling of the message on the dispatcher queue thread.

#### 2.3.5 Future (delayed-computation)

###### [in package SENTO.FUTURE with nicknames FUTURE]

• [class] FUTURE

The wrapped blackbird promise, here called future.
Not all features of blackbird's promise are supported.
This future wrapper changes the terminology. A future is a delayed computation. A promise is the fulfillment of the delayed computation.

• [macro] WITH-FUT &BODY BODY

Convenience macro for creating a future.

The future will be resolved with the result of the body form.

• [macro] WITH-FUT-RESOLVE &BODY BODY

Convenience macro for creating a future that must be resolved manually via fresolve.

This allows to spawn threads or other asynchronous operations as part of body. However, you have to resolve the future eventually by applying a result on resolve.

Example:

(with-fut-resolve
(lambda ()
(fresolve (do-some-lengthy-calculation)))))

• [function] MAKE-FUTURE EXECUTE-FUN

Creates a future. execute-fun is the lambda that is executed when the future is created. execute-fun takes a parameter which is the execute-fun funtion. execute-fun function takes the promise as parameter which is the computed value. Calling execute-fun with the promise will fulfill the future.
Manually calling execute-fun to fulfill the future is in contrast to just fulfill the future from a return value. The benefit of the execute-fun is flexibility. In a multi-threaded environment execute-fun could spawn a thread, in which case execute-fun would return immediately but no promise-value can be given at that time. The execute-fun can be called from a thread and provide the promise.

Create a future with:

(make-future (lambda (execute-fun)
(let ((promise (delayed-computation)))
(sleep 0.5)
(funcall execute-fun promise))))))

• [function] COMPLETE-P FUTURE

Is future completed? Returns either t or nil.

• [macro] FCOMPLETED FUTURE (RESULT) &BODY BODY

Completion handler on the given future.

If the future is already complete then the body executed immediately.
result represents the future result. body is executed when future completed.

Example:

(fcompleted (with-fut
(sleep .5)
1)
(result)
(format t "Future result ~a~%" result))

• [function] FRESULT FUTURE

Get the computation result. If not yet available :not-ready is returned.

• [macro] FMAP FUTURE (RESULT) &BODY BODY

fmap maps a future.

future is the future that is mapped. result is the result of the future when it completed. body is the form that executes when the future is completed. The result of body generates a new future.

Example:

(fmap (with-fut 0) (result)
(1+ result))

• [macro] FRECOVER FUTURE &REST HANDLER-FORMS

Catch errors in futures using frecover It works similar to handler-case.

Example:

(fresult
(frecover
(-> (with-fut 0)
(fmap (value)
(declare (ignore value))
(error "foo")))
(fmap (value)
(+ value 1))))
(error (c) (format nil "~a" c))))

### 2.4 Agent

###### [in package SENTO.AGENT with nicknames AGT]

• [function] MAKE-AGENT STATE-FUN &OPTIONAL ACTOR-CONTEXT (DISPATCHER-ID :SHARED)

Makes a new agent instance.

state-fun is a function that takes no parameter and provides the initial state of the agent as return value.

actor-context: optionally specify an asys:actor-system as ac:actor-context. If specified the agent will be registered in the system and destroyed with it should the asys:actor-system be destroyed. In addition the agent will use the systems shared message dispatcher and will not create it's own.

dispatcher-id: the dispatcher is configurable. Default is :shared. But you may use also :pinned or a custom configured one. Be aware that :shared of a custom dispatcher only works if an actor-context was specified.

• [function] AGENT-GET AGENT GET-FUN

Gets the current state of the agent. get-fun must accept one parameter. That is the current-state of the agent. To return the current state get-fun may be just the identity function.

• [function] AGENT-UPDATE AGENT UPDATE-FUN

Updates the agent state. update-fun must accept one parameter. That is the current state of the agent. The return value of update-fun will be taken as the new state of the agent.

• [function] AGENT-UPDATE-AND-GET AGENT UPDATE-FUN

Updates the agent state. update-fun must accept one parameter. That is the current state of the agent. The return value of update-fun will be taken as the new state of the agent. This function makes the update and returns the new value.

• [function] AGENT-STOP AGENT

Stops the message handling of the agent.

#### 2.4.1 Hash-table agent

###### [in package SENTO.AGENT.HASH with nicknames AGTHASH]

• [function] MAKE-HASH-AGENT CONTEXT &KEY INITIAL-HASH-TABLE (ERROR-FUN NIL) (DISPATCHER-ID :SHARED)

Creates an agent that wraps a CL hash-table.

context: something implementing ac:actor-context protocol like asys:actor-system. Specifying nil here creates an agent outside of an actor system. The user has to take care of that himself.
initial-hash-table: specify an initial hash-table.
error-fun: a 1-arrity function taking a condition that was raised. Use this to get notified of error when using the update functions of the agent.
dispatcher-id: a dispatcher. defaults to :shared.

• [function] AGENT-DOHASH FUN HASH-AGENT

'Do' arbitrary atomic operation on the hash-table.

fun: is a 1-arity function taking the hash-table. This function can operate on the hash-table without interference from other threads. The result of this function must be a hash-table.
hash-agent: is the hash-agent instance.

The result of agent-dohash is T.

#### 2.4.2 Array/Vector agent

###### [in package SENTO.AGENT.ARRAY with nicknames AGTARRAY]

• [function] MAKE-ARRAY-AGENT CONTEXT &KEY INITIAL-ARRAY (ERROR-FUN NIL) (DISPATCHER-ID :SHARED)

Creates an agent that wraps a CL array/vector.

context: something implementing ac:actor-context protocol like asys:actor-system. Specifying nil here creates an agent outside of an actor system. The user has to take care of that himself.
initial-array: specify an initial array/vector.
error-fun: a 1-arrity function taking a condition that was raised. Use this to get notified of error when using the update functions of the agent.
dispatcher-id: a dispatcher. defaults to :shared.

• [function] AGENT-ELT INDEX ARRAY-AGENT

Retrieves the value of the specified index of the array. agent-elt allows setfing like:

(setf (agent-elt 0 cut) 11)

index: the index to retrieve.
array-agent: the array agent instance.

In case of error agent-elt returns the error condition that elt raises.

The setf functionality will call err-fun on error if it has been configured.

• [function] AGENT-PUSH ITEM ARRAY-AGENT

Pushes a value to the array/vector. Internally uses vector-push-extend, so the array must have a fill-pointer.

item: item to push.
array-agent: the array agent instance.

On error it will call err-fun with the raised condition, if err-fun has been configured.

• [function] AGENT-PUSH-AND-GETIDX ITEM ARRAY-AGENT

Pushes item to the array. This function is similar to agent-push but returns the index of the pushed value similar as vector-push does. Therefore it is based on the somewhat slower ask-s actor pattern. So if you don't care about the new index of the pushed item use agent-push instead. But this one is able to immediately return error conditions that may occur on vector-push.

item: item to push.
array-agent: the array agent instance.

• [function] AGENT-POP ARRAY-AGENT

Pops from array and returns the popped value. Internally uses vector-pop, so the array must have a fill-pointer. In case of error from using vector-pop the condition is returned.

array-agent: the array agent instance.

• [function] AGENT-DELETE ITEM ARRAY-AGENT &REST DELETE-ARGS

Deletes item from array. Internally uses delete. Returns T.

item: the item to delete.
array-agent: the array agent instance.
delete-args: any arguments passed on to delete.

• [function] AGENT-DOARRAY FUN ARRAY-AGENT

'Do' arbitrary atomic operation on the array.

fun: is a 1-arity function taking the array. This function can operate on the array without interference from other threads. The result of this function must be an array which will be the new agent state.
array-agent: is the array-agent instance.

The result of agent-doarray is T.

### 2.5 Stashing

###### [in package SENTO.STASH with nicknames STASH]

• [class] STASHING

stashing is a mixin class to act:actor. It can 'stash' away arriving messages which should not be handled now, but later, after the actor is 'able' to handle them. Create an actor class that can stash like this:

(defclass stash-actor (actor stashing) ())

Then create an actor by specifying this type:

(actor-of system
:type 'stash-actor
...))

For stash and unstash see function descriptions below.

The main use-case is for act:tell and act:ask. act:ask-s will not work. timeouts are ignored because it is not clear how long stashed messages will reside in stash. However the sender, if given (on act:tell), is preserved.

• [function] STASH MSG

Stash msg for later unstash. On stashing a message the actor should respond with: (cons :no-reply state) to avoid returning a response to sender (if given).

This function is expected to be run from within 'receive' function.

• [function] UNSTASH-ALL

Unstash all messages. Messages are re-submitted to the actor in the order they were stashed. Resubmitting means they are added to the end of the queue like any ordinary message would.

This function is expected to be run from within 'receive' function.

### 2.6 Dispatcher

###### [in package SENTO.DISPATCHER with nicknames DISP]

• [reader] IDENTIFIER DISPATCHER-BASE (:IDENTIFIER = NIL)

Returns the identifier of the dispatcher.

• [generic-function] DISPATCH DISPATCHER DISPATCHER-EXEC-FUN

Dispatches a function (dispatch-exec-fun) to a worker of the dispatcher to execute there. dispatch does a ask-s to a dispatcher worker, which means this call will block.

• [generic-function] DISPATCH-ASYNC DISPATCHER DISPATCHER-EXEC-FUN

Dispatches a function to a worker of the dispatcher to execute there. dispatch-async does a tell to a dispatcher worker and is asynchronous.

• [generic-function] STOP DISPATCHER

Stops the dispatcher. Stops all workers.

• [generic-function] WORKERS DISPATCHER

Returns the workers of this dispatcher. But better do not touch them. Only use the defined interface here to talk to them.

• [function] MAKE-DISPATCHER-WORKER NUM ACTOR-CONTEXT DISPATCHER-IDENT

Constructor for creating a worker. num only has the purpose to give the worker a name which includes a number. dispatcher-ident is the dispatcher identifier.

### 2.7 Router

###### [in package SENTO.ROUTER with nicknames ROUTER]

• [class] ROUTER

A router combines a pool of actors and implements the actor-api protocol. So a tell, ask-s and ask is delegated to one of the routers routees. While a router implements parts of the actor protocol it doesn't implement all. I.e. a router cannot be watched. A router strategy defines how one of the actors is determined as the forwarding target of the message.

• [function] MAKE-ROUTER &KEY (STRATEGY :RANDOM) (ROUTEES NIL)

Default constructor of router. Built-in strategies: :random, :round-robin. Specify your own strategy by providing a function that takes a fixnum as parameter which represents the number of routees and returns a fixnum that represents the index of the routee to choose.

Specify routees if you know them upfront.

Adds a routee/actor to the router.

• [function] STOP ROUTER

Stops all routees.

• [function] ROUTEES ROUTER

Returns the routees as list.

• [method] TELL (SELF ROUTER) MESSAGE

Posts the message to one routee. The routee is chosen from the router strategy. Otherwise see: act:tell.

• [method] ASK-S (SELF ROUTER) MESSAGE

Posts the message to one routee. The routee is chosen from the router strategy. Otherwise see: act:ask-s.

• [method] ASK (SELF ROUTER) MESSAGE

Posts the message to one routee. The routee is chosen from the router strategy. Otherwise see: act:ask.

### 2.8 Eventstream

###### [in package SENTO.EVENTSTREAM with nicknames EV]

• [class] EVENTSTREAM

Eventstream facility allows to post/publish messages/events in the asys:actor-system and actors that did subscribe, to listen on those events.

The eventstream is driven by an actor. The processing of the sent events is guaranteed to be as they arrive.

Events can be posted as plain strings, as lists, or as objects of classes. The subscriber has a variaty of options to define what to listen for.

For example: a subscriber wants to listen to events/messages with the string "Foo". The subscriber is then only notified when events are posted with the exact same string.

See more information at the ev:subscribe function.

• [function] MAKE-EVENTSTREAM ACTOR-CONTEXT &REST CONFIG

Creating an eventstream is done by the asys:actor-system which is then available system wide. But in theory it can be created individually by just passing an ac:actor-context (though I don't know what would be the reason to create an eventstream for the context of a single actor. Maybe to address only a certain hierarchy in the actor tree.)

• actor-context: the ac:actor-context where the eventstream actor should be created in.

• config: is a plist with the :dispatcher-id key and a dispatcher id as value. Defaults to :shared. This dispatcher type should be used by the actor.

• [generic-function] SUBSCRIBE EVENTSTREAM SUBSCRIBER &OPTIONAL PATTERN

Subscribe to the eventstream to receive notifications of certain events or event types.

subscriber must be an actor (or agent).

The pattern can be:

• nil: receive all events posted to the eventstream.

• a type, class type: this allows to get notifications when an instance of this type, or class type is posted. I.e. if you want to listen to all string messages posted to the ev, thewn subscribe to 'string. Or if you want to listen to all lists, subscribe with 'cons.

• a symbol or global symbol: if posted message is a symbol or global symbol then the symbols are compared (eq).

• a string: in which case an exact string comparison is made for a string message that is posted to the eventstream (string=).

• a list: if subscription if for a list structure, and the posted message is also a list structure, then a structure comparison (equalp) is made.

• [generic-function] UNSUBSCRIBE EVENTSTREAM UNSUBSCRIBER

Unsubscribe from the eventstream. No more events will be received then.

• [generic-function] PUBLISH EVENTSTREAM MESSAGE

Publish an event/message to the eventstream. Subscribers may receive notification if they registered for the right message pattern.

• [macro] WITH-CONTEXT (CONTEXT &OPTIONAL (DISPATCHER :SHARED)) &BODY BODY

with-context creates an environment where the tasks package functions should be used in. context can be either an asys:actor-system, an ac:actor-context, or an act:actor (or subclass). dispatcher specifies the dispatcher where the tasks is executed in (like thread-pool). The tasks created using the tasks functions will then be created in the given context.

Example:

;; create actor-system
(defparameter *sys* (make-actor-system))

(with-context (*sys*)
(task-yield (lambda () (+ 1 1))))

=> 2 (2 bits, #x2, #o2, #b10)


Since the default :shared dispatcher should mainly be used for the message dispatching, but not so much for longer running tasks it is possible to create an actor system with additional dispatchers. This additional dispatcher can be utilized for tasks. Be aware that the config as used below is merged with the asys:*default-config* which means that the dispatcher :foo here is really an additional dispatcher.

;; create actor-system with additional (custom) dispatcher
(defparameter *sys* (asys:make-actor-system '(:dispatchers (:foo (:workers 16)))))

(with-context (*sys* :foo)
(task-yield (lambda () (+ 1 1))))


• [function] TASK-YIELD FUN &OPTIONAL TIME-OUT

task-yield runs the given function fun by blocking and waiting for a response from the task, or until the given timeout was elapsed. fun must be a 0-arity function.

A normal response from the actor is passed back as the response value. If the timeout elapsed the response is: (values :handler-error miscutils:ask-timeout).

Example:

;; create actor-system
(defparameter *sys* (make-actor-system))

(with-context (*sys*)
(task-yield (lambda () (+ 1 1))))

=> 2 (2 bits, #x2, #o2, #b10)

task-start runs the given function fun asynchronously. fun must be a 0-arity function. Use this if you don't care about any response or result, i.e. for I/O side-effects. It returns (values :ok <task>).  is in fact an actor given back as reference. The task is automatically stopped and removed from the context and will not be able to handle requests.

• [function] TASK-ASYNC FUN &KEY ON-COMPLETE-FUN

task-async schedules the function fun for asynchronous execution. fun must be a 0-arity function. on-complete-fun is a 1-arity completion handler function. When called the result is delivered. The completion handler function parameter may also be a (cons :handler-error condition) construct in case an error happened within the message handling.

Using task-async provides two alternatives:

In fact it is possible to call task-await as well, but then you probably don't need a completion handler. Using the completion handler makes the processing complete asynchronous.

The result of task-async is a task. Store this task for a call to task-async (even with or without using on-complete-fun). When not using on-complete-fun users must call either task-await or task-shutdown for the task to be cleaned up. When using on-complete-fun this is done for you.

Example:

;; create actor-system
(defparameter *sys* (make-actor-system))

(with-context (*sys*)
(let ((x (task-async (lambda () (some bigger computation))))
(y 1))

;; use-case with on-complete-fun
(do-something-with result))

(with-context (*sys*)
:on-complete-fun #'my-task-completion))

task-await waits (by blocking) until a result has been generated for a previous task-async by passing the task result of task-async to task-await. Specify time-out in seconds. If task-await times out a (cons :handler-error 'ask-timeout) will be returned. task-await also stops the task that is the result of task-async, so it is of no further use.

task-shutdown shuts down a task in order to clean up resources.

task-async-stream concurrently applies fun on all elements of lst. fun must be a one-arity function taking an element of lst.

The concurrency depends on the number of available :shared dispatcher workers. Each element of lst is processed by a worker of the asys:actor-systems :shared dispatcher. If all workers are busy then the computation of fun is queued.

Example:

;; create actor-system
(defparameter *sys* (make-actor-system))

(with-context (*sys*)
(->>
'(1 2 3 4 5)
(reduce #'+)))

=> 20 (5 bits, #x14, #o24, #b10100)

### 2.10 Config

###### [in package SENTO.CONFIG with nicknames CONFIG]

• [function] CONFIG-FROM CONFIG-STRING

Parses the given config-string, represented by common lisp s-expressions. The config is composed of plists in a hierarchy.

This function parses (run through cl:read) the given config string. The config string can be generated by:

(let ((*print-case* :downcase))
(prin1-to-string '(defconfig
(:foo 1
:bar 2))))

Or just be given by reading from a file. Notice the 'config' s-expr must start with the root car 'defconfig'.

• [function] RETRIEVE-SECTION CONFIG SECTION

Retrieves the given named section which should be a (global) symbol (a key). A section usually is a plist with additional configs or sub sections. This function looks only in the root hierarchy of the given config.

• [function] RETRIEVE-VALUE SECTION KEY

Retrieves the value for the given key and section.

• [function] MERGE-CONFIG CONFIG FALLBACK-CONFIG

Merges config. config specifies a config that overrides what exists in fallback-config. fallback-config is a default. If something doesn't exist in config it is taken from fallback-config. Both config and fallback-config must be plists, or a 'config' that was the output of config-from`.