{"id":33231745,"url":"https://github.com/mdbergmann/cl-gserver","last_synced_at":"2026-01-24T00:48:13.194Z","repository":{"id":41710352,"uuid":"243614333","full_name":"mdbergmann/cl-gserver","owner":"mdbergmann","description":"Sento - Actor framework featuring actors and agents for easy access to state and asynchronous operations.","archived":false,"fork":false,"pushed_at":"2025-02-04T19:41:16.000Z","size":3671,"stargazers_count":211,"open_issues_count":13,"forks_count":14,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-02-04T20:32:05.914Z","etag":null,"topics":["actor","common-lisp","concurrency","event-driven","genserver","reactive"],"latest_commit_sha":null,"homepage":"https://mdbergmann.github.io/cl-gserver/index.html","language":"Common Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mdbergmann.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-02-27T20:51:42.000Z","updated_at":"2025-02-04T19:41:19.000Z","dependencies_parsed_at":"2024-01-31T12:05:01.826Z","dependency_job_id":"1301df2a-7f4e-49a6-a467-017c4cb4882b","html_url":"https://github.com/mdbergmann/cl-gserver","commit_stats":null,"previous_names":[],"tags_count":44,"template":false,"template_full_name":null,"purl":"pkg:github/mdbergmann/cl-gserver","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdbergmann%2Fcl-gserver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdbergmann%2Fcl-gserver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdbergmann%2Fcl-gserver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdbergmann%2Fcl-gserver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mdbergmann","download_url":"https://codeload.github.com/mdbergmann/cl-gserver/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdbergmann%2Fcl-gserver/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":284759739,"owners_count":27058842,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-16T02:00:05.974Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["actor","common-lisp","concurrency","event-driven","genserver","reactive"],"created_at":"2025-11-16T18:00:20.905Z","updated_at":"2025-11-16T19:01:19.707Z","avatar_url":"https://github.com/mdbergmann.png","language":"Common Lisp","readme":"![CI](https://github.com/mdbergmann/cl-gserver/workflows/CI/badge.svg?branch=master)\n\n### Introduction - Actor framework featuring actors and agents\n\n__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.\n\nSento features:\n\n- Actors with `ask` (`?`) and `tell` (`!`) operations. `ask` can be asynchronous or synchronous.\n- 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.\n- FSM (Finite-State-Machine). A ready fsm framework.\n- Router: Router offers a similar interface as Actor with `ask` and `tell` but collects multiple Actors for load-balancing.\n- 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.\n- Tasks: a simple API for concurrency.\n- Futures library\n\n(Please also checkout the API [documentation](https://mdbergmann.github.io/cl-gserver/index.html) for further information)\n(for migrations from Sento v2, please check below migration guide)\n\n### Projects using Sento (for example usage):\n\n- [Chipi automation tool](https://github.com/mdbergmann/chipi): Actors used for foundational primitives like 'items' and 'persistences'.\n- [KNX-conn](https://github.com/mdbergmann/knx-conn): Used for asynchronous reading/writing from/to KNX bus\n- [Hunchentoot taskmanager](https://github.com/mdbergmann/cl-tbnl-gserver-tmgr): High throughput Hunchentoot task manager\n\n### Intro\n\n#### Creating an actor-system\n\nThe first thing you wanna do is to create an actor system.\nIn simple terms, an actor system is a container where all actors live in. So at any time the actor system knows which actors exist.\n\nTo create an actor system we can first change package to `:sento-user` because it imports the majority of necessary namespaces fopr convenience. Then, do:\n\n```elisp\n(defvar *system* (make-actor-system))\n```\n\nWhen we look at `*system*` in the repl we see some information of the actor system:\n\n```plain\n#\u003cACTOR-SYSTEM config: (DISPATCHERS\n                        (SHARED (WORKERS 4 STRATEGY RANDOM))\n                        TIMEOUT-TIMER\n                        (RESOLUTION 500 MAX-SIZE 1000)\n                        EVENTSTREAM\n                        (DISPATCHER-ID SHARED)\n                        SCHEDULER\n                        (ENABLED TRUE RESOLUTION 100 MAX-SIZE 500)\n                        ), user actors: 0, internal actors: 5\u003e\n```\n\nThe `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).\n\nThere 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.\n\nUsing 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.\n\nActors 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.\n\nI.e. to shutdown the actor system one has to execute: `(ac:shutdown *system*)`.\n\n\n#### Creating and using actors\n\nNow we want to create actors.\n\n```elisp\n(actor-of *system* :name \"answerer\"\n  :receive\n  (lambda (msg)\n    (let ((output (format nil \"Hello ~a\" msg)))\n        (reply output))))\n```\n\nThis 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.\n\nThe `: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).\n\n`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.\n\nThe 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.\n\nStore the actor to a variable `*answerer*` by just doing `(defparameter *answerer* *)` in the repl where `*` resembled the last result (the actor instance in our case). By evaluating the variable (`*answerer*`) we can see the printed object:\n\n```\n#\u003cACTOR path: /user/answerer, cell: #\u003cACTOR answerer, running: T, state: NIL, message-box: #\u003cSENTO.MESSAGEB:MESSAGE-BOX/DP mesgb-1356, processed messages: 1, max-queue-size: 0, queue: #\u003cSENTO.QUEUE:QUEUE-UNBOUNDED 82701A6D13\u003e\u003e\u003e\u003e\n```\n\nWe'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.\n\nNow, when sending a message using 'ask' pattern to the above actor like so:\n\n```elisp\n(? *answerer* \"FooBar\")\n```\n\nwe'll get a 'future' as result, because `?`/`ask` is asynchronous.\n\n```plain\n#\u003cFUTURE promise: #\u003cBLACKBIRD-BASE:PROMISE\nfinished: NIL\nerrored: NIL\nforward: NIL 80100E8B7B\u003e\u003e\n```\n\nWe can check for a 'future' result. By now the answer from the `*answerer*` (via `reply`) should be available:\n\n```plain\nUSER\u003e (fresult *)\n\"Hello FooBar\"\n```\n\nIf 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`.\n\nA 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:\n\n```elisp\n(fcompleted\n     (? *answerer* \"Buzz\")\n     (result)\n   (format t \"The answer is: ~a~%\" result))\n```\n\nWhich will asynchronously print _\"The answer is: Hello Buzz\"_ after a short while.\nThis will also work when the `ask`/`?` was used with a timeout, in which case `result` will be a tuple of `(:handler-error . \u003cask-timeout condition\u003e)` if the operation timed out.\n\n\n#### Creating child actors\n\nTo build actor hierarchies one has to create actors in actors. This is of course possible. There are two options for this.\n\n1. Actors are created as part of `actor-of`s `:init` function like so:\n\n```elisp\n(actor-of *system* \n          :name \"answerer-with-child\"\n          :receive\n          (lambda (msg)\n            (let ((output (format nil \"Hello ~a\" msg)))\n              (reply output)))\n          :init\n          (lambda (self)\n            (actor-of self \n                      :name \"child-answerer\"\n                      :receive \n                      (lambda (msg)\n                        (let ((output (format nil \"Hello-child ~a\" msg)))\n                          (format nil \"~a~%\" output))))))\n```\n\nNotice the context for creating 'child-answerer', it is `self`, which is 'answerer-with-child'.\n\n2. Or it is possible externally like so:\n\n```elisp\n(actor-of *answerer* :name \"child-answerer\"\n    :receive \n    (lambda (msg)\n        (let ((output (format nil \"~a\" \"Hello-child ~a\" msg)))\n            (format nil \"~a~%\" output))))\n```\n\nThis uses `*answerer*` context as parameter of `actor-of`. But has the same effect as above.\n\nNow we can check if there is an actor in context of 'answerer-with-child':\n\n```plain\nUSER\u003e (all-actors *actor-with-child*)\n(#\u003cACTOR path: /user/answerer-with-child/child-answerer, cell: #\u003cACTOR child-answerer, running: T, state: NIL, message-box: #\u003cSENTO.MESSAGEB:MESSAGE-BOX/DP mesgb-1374, processed messages: 0, max-queue-size: 0, queue: #\u003cSENTO.QUEUE:QUEUE-UNBOUNDED 8200A195FB\u003e\u003e\u003e\u003e)\n```\n\nThe 'path' is what we expected: '/user/answerer-with-child/child-answerer'.\n\n#### Ping Pong\n\nAnother example that only works with `tell`/`!` (fire and forget).\n\nWe have those two actors.\n\nThe 'ping' actor:\n\n```elisp\n(defparameter *ping*\n  (actor-of *system*\n            :receive\n            (lambda (msg)\n              (cond\n                ((consp msg)\n                 (case (car msg)\n                   (:start-ping\n                    (progn\n                      (format t \"Starting ping...~%\")\n                      (! (cdr msg) :ping *self*)))))\n                ((eq msg :pong)\n                 (progn\n                   (format t \"pong~%\")\n                   (sleep 2)\n                   (reply :ping)))))))\n```\n\nAnd the 'pong' actor:\n\n```elisp\n(defparameter *pong*\n  (actor-of *system*\n            :receive\n            (lambda (msg)\n              (case msg\n                (:ping\n                 (progn\n                   (format t \"ping~%\")\n                   (sleep 2)\n                   (reply :pong)))))))\n```\n\nThe 'ping' actor understands a `:start-ping` message which is a `cons` and has as `cdr` the 'pong' actor instance.\nIt also understands a `:pong` message as received from 'pong' actor.\n\nThe 'pong' actor only understands a `:ping` message. Each of the actors respond with either `:ping` or `:pong` respectively after waiting 2 seconds.\n\nWe trigger the ping-pong by doing:\n\n```elisp\n(! *ping* `(:start-ping . ,*pong*))\n```\n\nAnd then see in the console like:\n\n```plain\nStarting ping...\nping\npong\nping\n...\n```\n\nTo stop the ping-pong one just has to send `(! *ping* :stop)` to one of them.\n\n##### Stopping actors\n\nSending the:\n- `:stop` message will completely stop the actors message processing. No new messages will be accepted, but all messages in the queue are still processed.\n- `:terminate` message will also stop the actor from accepting more messages but it will also discard any queued messages. Only the one that is currently being processed will be allowed to finish.\n\nIt is also possible to call `act-cell:stop` method on the actor. It has the same effect as sending `:terminate`.\n\n##### Synchronous ask\n\nAt last an example for the synchronous 'ask', `ask-s`. It is insofar similar to `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.  \nBeware that `ask-s` will dead-lock your actor when `ask-s` is used to call itself.  \nLet's make an example:\n\n```elisp\n(defparameter *s-asker*\n  (actor-of *system*\n            :receive\n            (lambda (msg)\n              (cond\n                ((stringp msg)\n                 (format nil \"Hello ~a\" msg))\n                (t (format nil \"Unknown message!\"))))))\n```\n\nSo we can do:\n\n```plain\nUSER\u003e (ask-s *s-asker* \"Foo\")\n\"Hello Foo\"\nUSER\u003e (ask-s *s-asker* 'foo)\n\"Unknown message!\"\n```\n\n#### Dispatchers `:pinned` vs. `:shared`\n\nDispatchers 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.\n\nSo `:pinned` and `:shared` are types of dispatchers. `:pinned` spawns its own mailbox thread, `:shared` uses a worker pool to handle the mailbox messages.\n\nBy 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.\n\nWhen creating an actor it is possible to specify the `dispatcher-id`. This parameter specifies which 'dispatcher' should handle the mailbox queue/messages.\n\nPlease see below for more info on dispatchers.\n\n#### Finding actors in the context\n\nIf 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](https://mdbergmann.github.io/cl-gserver/index.html#SENTO.ACTOR-CONTEXT:FIND-ACTORS%20GENERIC-FUNCTION).\n\n#### Mapping futures with fmap\n\nLet's asume we have such a simple actor that just increments the value passed to it.\n\n```\n(defparameter *incer*\n  (actor-of *system*\n            :receive (lambda (value)\n                       (reply (1+ value)))))\n```\n\nSince `ask` returns a future it is possible to map multiple `ask` operations like this:\n\n```\n(-\u003e (ask *incer* 0)\n  (fmap (result)\n      (ask *incer* result))\n  (fmap (result)\n      (ask *incer* result))\n  (fcompleted (result)\n      (format t \"result: ~a~%\" result)\n    (assert (= result 3))))\n```\n\n\n#### ask-s and ask with timeout\n\nA timeout (in seconds) can be specified for both `ask-s` and\n`ask` and is done like so:\n\nTo demonstrate this we could setup an example 'sleeper' actor:\n\n```elisp\n(ac:actor-of *system* \n    :receive \n    (lambda (msg)\n        (sleep 5)))\n```\n\nIf we store this to `*sleeper*` and do the following, the\n`ask-s` will return a `handler-error` with an\n`ask-timeout` condition.\n\n```elisp\n(act:ask-s *sleeper* \"Foo\" :time-out 2)\n```\n\n```\n(:HANDLER-ERROR . #\u003cCL-GSERVER.UTILS:ASK-TIMEOUT #x30200319F97D\u003e)\n```\n\nThis works similar with the `ask` only that the future will\nbe fulfilled with the `handler-error` `cons`.\n\nTo get a readable error message of the condition we can do:\n\n```\nCL-USER\u003e (format t \"~a\" (cdr *))\nA timeout set to 2 seconds occurred. Cause: \n#\u003cBORDEAUX-THREADS:TIMEOUT #x302002FAB73D\u003e \n```\n\nNote that `ask-s` uses the calling thread for the timeout checks.  \n`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](https://mdbergmann.github.io/cl-gserver/index.html#SENTO.ACTOR-SYSTEM:*DEFAULT-CONFIG*%20VARIABLE) for more details.\n\n#### Long running and asynchronous operations in `receive`\n\nBe careful with doing long running computations in the `receive` function message handler, because it will block message processing. It is advised to use a separate thread, third-party thread-pool, a library like *lparallel*, or the provided 'Tasks' API using a dedicated dispatcher to do the computations with, and return early from the `receive` message handler.\n\nThe 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*`.\n\nDue 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.\n\nFor 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 (receive function):\n\n```elisp\n(lambda (msg)\n  (case msg\n    (:do-lengthy-op\n     (let ((sender *sender*))\n       ;; do lengthy computation\n       (reply :my-later-reply sender)))\n    (otherwise\n      ;; do other non async stuff\n      (reply :my-reply))))\n```\n\nNotice 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`.\n\nSee [this test](../tests/spawn-in-receive-test.lisp) for more info.\n\nNOTE: you should not change actor state from within an asynchronously executed operation in `receive`. This is not thread-safe. The pattern for this case it to send a message to `self` and have a message handler case that will change the actor state. This will ensure that actor state is always changed in a thread-safe way.\n\n#### Changing behavior\n\nAn actor can change its behavior. The behavior is just a lambda similar as the 'receive' function taking the message as parameter.\n\nThe default behavior of the actor is given on actor construction using `:receive` key.\n\nDuring the lifetime of an actor the behavior can be changed using `become`. `unbecome` will restore the default behavior.\n\nHere is an example:\n\n```elisp\n(ac:actor-of *system*\n             :receive\n             (lambda (msg)\n               (case msg\n                 (:open\n                  (progn\n                    (unstash-all)\n                    (become (lambda (msg)\n                              (case msg\n                                (:write\n                                 ;; do something\n                                 )\n                                (:close\n                                 (unbecome))\n                                (otherwise\n                                 (stash msg)))))))\n                 (otherwise (stash msg)))))\n```\n\n#### Stashing messages\n\nStashing 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.\n\nSee: API [documentation](https://mdbergmann.github.io/cl-gserver/index.html#SENTO.STASH:STASHING%20CLASS) for more info.\n\n#### Creating actors without a system\n\nIt is still possible to create actors without a system. This is how you do it:\n\n```elisp\n;; make an actor\n(defvar *my-actor* (act:make-actor (lambda (msg)\n                                     (format t \"FooBar\"))\n                                   :name \"Lone-actor\"))\n;; setup a thread based message box\n(setf (act-cell:msgbox *my-actor*) \n      (make-instance 'mesgb:message-box/bt))\n```\n\nYou have to take care yourself about stopping the actor and freeing resources.\n\n### Agents\n\nAn Agent is a specialized Actor. It is meant primarily for maintaining state and comes with some conveniences to do that.\n\nTo use an Agent import `sento.agent` package.\n\nThere is no need to subclass an Agent. Rather create a facade to customize an agent. See below.\n\nAn Agent provides three functions to use it.\n\n- `make-agent` creates a new agent. Optionally specify an `actor-context` or define the kind of dispatcher the agent should use.\n- `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.\n- `agent-update` updates the state of the agent\n- `agent-update-and-get` updates the agent state and returns the new state.\n\nAll 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.\nThis parameter represents the current state of the agent.\n\nLet's make a simple example:\n\nFirst create an agent with an initial state of `0`.\n\n```elisp\n(defparameter *my-agent* (make-agent (lambda () 0)))\n```\n\nNow update the state several times (`agent-update` is asynchronous and returns `t` immediately):\n\n```elisp\n(agent-update *my-agent* (lambda (state) (1+ state)))\n```\n\nFinally get the state:\n\n```elisp\n(agent-get *my-agent* #'identity)\n```\n\nThis `agent-get` just uses the `identity` function to return the state as is.\n\nSo this simple agent represents a counter.\n\nIt is important to note that the retrieves state, i.e. with `identity` should not be modified outside the agent.\n\n#### Using an agent within an actor-system\n\nThe `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.\n\nIt also implies that the agent is destroyed then the actor-system is destroyed.\n\nHowever, 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.\n\n#### Wrapping an agent\n\nWhile 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.\n\nFor example could a facade for the counter above look like this:\n\n```elisp\n(defvar *counter-agent* nil)\n\n(defun init-agent (initial-value)\n  (setf *counter-agent* (make-agent (lambda () initial-value))))\n\n(defun increment () (agent-update *counter-agent* #'1+))\n(defun decrement () (agent-update *counter-agent* #'1-))\n(defun counter-value () (agent-get *counter-agent* #'identity))\n```\n\nAlternatively, one can wrap an agent inside a class and provide methods for simplified access to it.\n\n### Finite State Machine (FSM)\n\nThe Finite State Machine (FSM) model is a computational framework designed to model and manage systems that transition between various states based on inputs or events. This structured approach facilitates the handling of complex logic through defined state transitions and events.\n\n#### Creating an FSM\n\nTo create an FSM, use the `make-fsm` function, which initializes an actor with state management capabilities.\n\n(Additional API documentation can be found [here](https://mdbergmann.github.io/cl-gserver/index.html#SENTO.FSM:@FSM%20MGL-PAX:SECTION).)\n(The API is quite stable, but since this is a new feature minor changes are possible.)\n\n1. **actor-context**: Specifies where the FSM is created, which can be an actor, an actor-context, or an actor-system.\n2. **name**: A string that names the FSM.\n3. **start-with**: A cons cell representing the initial state and associated data.\n4. **event-handling**: A function structured using FSM macros to define the FSM's behavior upon receiving events.\n\n#### Example: Traffic Light Controller FSM with Timeouts\n\nThis FSM simulates a traffic light controller and includes comprehensive state and transition handling, including timeouts.\n\n```elisp\n(defun make-traffic-light-fsm (actor-context)\n  (make-fsm\n   actor-context\n   :name \"traffic-light-fsm\"\n   :start-with '(red . ())\n   :event-handling (lambda ()\n\n                     ;; Define behavior in each state using when-state\n                     (when-state ('red :timeout-s 10)\n                       (on-event ('timer) :state-timeout\n                         (goto-state 'green))\n                       (on-event ('manual-override)\n                         (stay-on-state '(:manual-override-engaged))\n                         (log:info \"Manual override activated\")))\n\n                     (when-state ('green :timeout-s 15)\n                       (on-event ('timer) :state-timeout\n                         (goto-state 'yellow)))\n\n                     (when-state ('yellow :timeout-s 5)\n                       (on-event ('emergency-stop)\n                         (goto-state 'red)\n                         (log:info \"Emergency stop activated\"))\n                       (on-event ('timer) :state-timeout\n                         (goto-state 'red)))\n\n                     ;; Handle state transitions\n                     (on-transition ('(red . green))\n                       (log:info \"Transition from red to green\"))\n\n                     (on-transition ('(green . yellow))\n                       (log:info \"Transition from green to yellow\"))\n\n                     (on-transition ('(yellow . red))\n                       (log:info \"Transition from yellow to red\"))\n\n                     ;; Stay on current state but update data\n                     (on-event ('maintenance-check)\n                       (stay-on-state '(:maintenance-required))\n                       (log:info \"Maintenance required\"))\n\n                     ;; Handle unhandled events with specific and general catch\n                     (when-unhandled ('unexpected-event)\n                       (log:warn \"Unexpected specific event caught\"))\n\n                     (when-unhandled (t :test #'typep)\n                       (log:warn \"Unhandled event caught: ~A\" *received-event*)))))\n```\n\n#### Example Breakdown\n\n- **actor-context**: Passed argument where the FSM operates.\n- **name**: The FSM is named \"traffic-light-fsm\".\n- **start-with**: The FSM begins in the 'red' state.\n- **event-handling**: Utilizes various macros:\n  - `when-state`: Defines actions specific to each state and handles timeouts.\n  - `on-event ('timer)`: Transitions the FSM to the next state on timer events; handles timeouts with `:state-timeout`.\n  - `goto-state`: Transitions the FSM to a new state, optionally updating data and logging.\n  - `stay-on-state ('maintenance-check)`: Remains in the current state while updating state data.\n  - `on-transition`: Logs state transitions.\n  - General `when-unhandled` using `typep`: Catches all other unhandled events and logs them using the dynamic variable `*received-event*` for context.\n\n#### Running the FSM\n\nAfter setup, the FSM processes events, transitioning as defined. This setup ensures responsive and structured event-driven state management.\n\nIncorporating timeout controls and a comprehensive fallback for unhandled events, this FSM elegantly manages complex state logic with powerful macro functionalities.\n\nFor more examples have a look at the [tests](tests/fsm-test.lisp).\n\n### Router\n\nA `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.\n\nRouters 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.\n\nThe `strategy` can be configured when creating a router using the constructors `\u0026key` 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.\n\nCurrently available strategies: `:random` and`:round-robin`.\n\nCustom strategies can be implemented.\n\n### Dispatchers\n\n#### :shared\n\nA `: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).\n\nUsing this dispatcher allows to create a large number of actors. The actors as such are generally very cheap.\n\n\u003cimg alt=\"\" src=\"./docs/disp_shared.png\" width=\"700\"/\u003e\n\u003cimg alt=\"\" src=\"disp_shared.png\" width=\"700\"/\u003e\n\n#### :pinned\n\nThe `: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.\n\nThis variant is slightly faster (see below) but requires one thread per actor.\n\n\u003cimg alt=\"\" src=\"./docs/disp_pinned.png\" width=\"700\"/\u003e\n\u003cimg alt=\"\" src=\"disp_pinned.png\" width=\"700\"/\u003e\n\n\n#### custom dispatcher\n\nIt 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`.\n\nWhen 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.\n\nA 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.\n\n### Eventstream\n\nThe 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.  \nThis allows to create event-based systems.\n\nHere is a simple example:\n\n```elisp\n(defparameter *sys* (asys:make-actor-system))\n\n(ac:actor-of *sys* :name \"listener\"\n  :init (lambda (self)\n          (ev:subscribe self self 'string))\n  :receive (lambda (msg)\n             (cond\n               ((string= \"my-message\" msg)\n                (format t \"received event: ~a~%\" msg)))))\n\n(ev:publish *sys* \"my-message\")\n```\n\nThis subscribes to all `'string` based events and just prints the message when received.  \nThe 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.\n\n```\nreceived event: my-message\n```\n\nSee the API [documentation](https://mdbergmann.github.io/cl-gserver/index.html#SENTO.EVENTSTREAM:EVENTSTREAM%20CLASS) for more details.\n\n### Tasks\n\n'tasks' is a convenience package that makes dealing with asynchronous and concurrent operations very easy.\n\nHere is a simple example:\n\n```elisp\n(defparameter *sys* (make-actor-system))\n\n(with-context (*sys*)\n  \n  // run something without requiring a feedback\n  (task-start (lambda () (do-lengthy-IO))\n  \n  // run asynchronous - with await\n  (let ((task (task-async (lambda () (do-a-task)))))\n    // do some other stuff\n    // eventually we need the task result\n    (+ (task-await task) 5))\n    \n  // run asynchronous with completion-handler (continuation)\n  (task-async (lambda () (some-bigger-computation))\n              :on-complete-fun\n              (lambda (result)\n                (do-something-with result)))\n\n  // concurrently map over the given list\n  (-\u003e\u003e \n    '(1 2 3 4 5)\n    (task-async-stream #'1+)\n    (reduce #'+)))\n\n=\u003e 20 (5 bits, #x14, #o24, #b10100)\n\n```\n\nAll 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.\n\nWhat 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.\n\nIt is possible to specify a second argument to the `with-context` macro to specify the dispatcher that should be used for the tasks.  \nThe concurrency here depends on the number of dispatcher workers.\n\nAs 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.\n\nBe 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.\n\nSee the API [documentation](https://mdbergmann.github.io/cl-gserver/index.html#SENTO.TASKS:WITH-CONTEXT%20MGL-PAX:MACRO) for more details.\n\n### Immutability\n\nSome 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.\n\n### Logging\n\nSento 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:\n\n```\n(log:config '(sento) :warn)\n```\n\nThis will tell log4cl to do any logging for sento in warn level.\n\n### Benchmarks\n\nHardware specs (M1)):\n\n-   Mac M1 Ultra, 64 GB RAM\n\n![](./docs/perf-M1Ultra.png)\n![](perf-M1Ultra.png)\n\nHardware specs (x86-64):\n\n-   iMac Pro (2017), 8 Core Xeon, 32 GB RAM\n\n![](./docs/perf-x86_64.png)\n![](perf-x86_64.png)\n\n\n**All**\n\nThe 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.\n\nOf 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.\n\n**SBCL (v2.4.9)**\n\nSBCL is very fast, but problematic in the synchronous ask case using :shared dispatcher.\n\n**LispWorks (8.0.1)**\n\nLispWorks is fast overall. Not as fast as SBCL. But it seems the GC is more robust, in particular on the `dispatched - ask`.\n\n**CCL (v1.13)**\n\nUnfortunately CCL doesn't work natively on M1 Apple CPU.\n\n**ABCL (1.9)**\n\nThe pleasant surprise was ABCL. While not being the fastest it is very robust.\n\n**Clasp 2.6.0**\n\nVery slow. Used default settings, as also for the other tests.\nMaybe something can be tweaked?\n\n### Migration guide for moving from Sento 2 to Sento 3\n\n- the receive function is now 1-arity. It only takes the a message parameter.\nPrevious 'self' and 'state' parameters are now accessible via `*self*` and `*state*`. The same applies to `become` function.\n\n- the return value of 'receive' function has always been a bit of an obstacle. So now it is ignored for `tell` and `ask`. In both cases a `reply` function 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`, but now it doesn't need to be a `cons`. Whatever is returned is received by `ask-s`.\n\n- 'utils' package has been split to 'timeutils' for i.e. ask-timeout condition, and 'miscutils' for i.e. filter function.\n\n### Version history\n\n**Version 3.4.2 (25.05.2025):** Forcefully stop actor threads on shutdown based on timeout.\n\n**Version 3.4.1 (30.01.2025):** More resiliancy in message-box (respawn thread if it was killed).\n\n**Version 3.4.0 (5.10.2024):** Finalized finite-state-machine (FSM) and documentation.\n\n**Version 3.3.3 (1.10.2024):** Bug fix for actor with dispatcher mailbox where the order of processing messages wasn't honoured.\n\n**Version 3.3.2 (14.8.2024):** Primarily documentation changes in regards to `future`\n\n**Version 3.3.0 (1.7.2024):** See: [Changelog](https://github.com/mdbergmann/cl-gserver/compare/3.2.1...3.3.0)\n\n**Version 3.2.0 (13.2.2024):** Message-box queue changes. SBCL now uses a separate fast CAS based queue coming as a contrib package. The other impls use a faster queue by default but still with locking. New benchmarks.\n\n**Version 3.1.0 (14.1.2024):** Added scheduler facility to actor-system that allows to schedule functions one or recurring. See API documentation for more info.\n\n**Version 3.0.4 (10.7.2023):** Allow additional initialization arguments be passed to actor. Wheel-time now knows CANCEL function. Partial fix for clasp (2.3.0).\n\n**Version 3.0.3 (1.4.2023):** Minor implementation changes regarding pre-start and after-stop.\n\n**Version 3.0.2 (6.4.2023):** Fix for actor stopping with 'wait'.\n\n**Version 3.0.0 (1.2.2023):** New major version. See migration guide if you have are migrating from version 2.\n\n**Version 2.2.0 (27.12.2022):** Added stashing and unstashing of messages.\n\n**Version 2.1.0 (17.11.2022):** Reworked the `future` package. Nicer syntax and futures can now be mapped.\n\n**Version 2.0.0 (16.8.2022):** Rename to \"Sento\". Incompatible change due to package names and system have changed.\n\n**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.\n\n**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.\n\n**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.\n\n**Version 1.11.1 (25.2.2022):** Minor additions to `actor-of` macro to allow specifying a `destroy` function.\n\n**Version 1.11.0 (16.1.2022):** Changes to `AC:FIND-ACTORS`. Breaking API change. See API documentation for details.\n\n**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.\n\n**Version 1.9.0:** Use wheel timer for `ask` timeouts.\n\n**Version 1.8.2:** atomic add/remove of actors in actor-context.\n\n**Version 1.8.0:** hash-agent interface changes. Added array-agent.\n\n**Version 1.7.6:** Added cl:hash-table based agent with similar API interface.\n\n**Version 1.7.5:** Allow agent to specify the dispatcher to be used.\n\n**Version 1.7.4:** more convenience additions for task-async (completion-handler)\n\n**Version 1.7.3:** cleaned up dependencies. Now sento works on SBCL, CCL, LispWorks, Allegro and ABCL\n\n**Version 1.7.2:** allowing to choose the dispatcher strategy via configuration\n\n**Version 1.7.1:** added possibility to create additional and custom dispatchers. I.e. to be used with `tasks`.\n\n**Version 1.7.0:** added tasks abstraction facility to more easily deal with asynchronous and concurrent operations.\n\n**Version 1.6.0:** added eventstream facility for building event based systems. Plus documentation improvements.\n\n**Version 1.5.0:** added configuration structure. actor-system can now be created with a configuration. More configuration options to come.\n\n**Version 1.4.1:** changed documentation to the excellent [mgl-pax](https://github.com/melisgl/mgl-pax)\n\n**Version 1.4:** convenience macro for creating actor. See below for more details\n\n**Version 1.3.1:** round-robin strategy for router\n\n**Version 1.3:** agents can be created in actor-system\n\n**Version 1.2:** introduces a breaking change\n\n`ask` has been renamed to `ask-s`.\n\n`async-ask` has been renamed to `ask`.\n\nThe proposed default way to query for a result from another actor should\nbe an asynchronous `ask`. `ask-s` (synchronous) is\nof course still possible.\n\n**Version 1.0** of `sento` library comes with quite a\nfew new features (compared to the previous 0.x versions). \nOne of the major new features is that an actor is not\nbound to it's own message dispatcher thread. Instead, when an\n`actor-system` is set-up, actors can use a shared pool of\nmessage dispatchers which effectively allows to create millions of\nactors.\n\nIt is now possible to create actor hierarchies. An actor can have child\nactors. An actor now can also 'watch' another actor to get notified\nabout it's termination.\n\nIt is also possible to specify timeouts for the `ask-s` and\n`ask` functionality.\n\nThis new version is closer to Akka (the actor model framework on the\nJVM) than to GenServer on Erlang. This is because Common Lisp from a\nruntime perspective is closer to JVM than to Erlang/OTP. Threads in\nCommon Lisp are heavy weight OS threads rather than user-space low\nweight 'Erlang' threads (I'd like to avoid 'green threads', because\nthreads in Erlang are not really green threads). While on Erlang it is\neasily possible to spawn millions of processes/threads and so each actor\n(GenServer) has its own process, this model is not possible when the\nthreads are OS threads, because of OS resource limits. This is the main\nreason for working with the message dispatcher pool instead.\n","funding_links":[],"categories":["Python ##","Interfaces to other package managers"],"sub_categories":["Third-party APIs"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmdbergmann%2Fcl-gserver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmdbergmann%2Fcl-gserver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmdbergmann%2Fcl-gserver/lists"}