SENTO-000 Sento Runtime Analysis
DONE Objective
Analyze Sento1 deeply enough to determine what can be composed with existing public APIs2, what requires a companion library3, and what requires changes to Sento core4.
The analysis focuses on supervised gservers5, per-domain dispatchers6, subdomains, managed routers7, overload handling8, event isolation, observability9, and secure remoting10.
Affected designs:
DONE Review Baseline
- Repository:
mdbergmann/cl-gserver - Reviewed branch:
master - Reviewed commit:
a889b8375709ca9396bba5d06b6e460689a23716 - Sento version:
3.4.3 - Sento remoting version:
0.1.0 - Review date:
2026-07-19
The latest reviewed commit fixes shutdown of actors whose bounded mailbox11 is already full. The fix catches queue-full-error while submitting the internal stop trigger.
DONE Repository Areas Reviewed
Core runtime
src/actor-system-api.lispsrc/actor-system.lispsrc/actor-context-api.lispsrc/actor-context.lispsrc/actor-api.lispsrc/actor.lispsrc/actor-cell.lispsrc/mbox/message-box.lispsrc/queue/queue.lispsrc/dispatcher-api.lispsrc/dispatcher.lispsrc/router.lispsrc/eventstream-api.lispsrc/eventstream.lispsrc/wheel-timer.lispsrc/tasks.lispsrc/fsm.lispsrc/stash.lisp
Remoting runtime
sento-remoting.asdsrc/remoting/remoting-api.lispsrc/remoting/remoting.lispsrc/remoting/remote-ref.lispsrc/remoting/envelope.lispsrc/remoting/serialization.lispsrc/remoting/transport.lispsrc/remoting/transport-tcp.lispsrc/remoting/tls.lispsrc/remoting/tls-pure.lispspecs/remoting.mdspecs/remoting-state.md
Tests and automation
tests/actor-tree-test.lisptests/actor-test.lisptests/message-box-test.lisptests/router-test.lisptests/dispatcher-test.lisptests/eventstream-test.lisptests/remoting/bench.lisp.github/workflows/CI.yml
DONE Actor-System Model
A Sento actor system12 creates:
/useractor context13/internalactor context- configured dispatchers
- one system event stream14
- one timeout wheel timer15
- one optional scheduler
The default shared dispatcher creates four workers.
Each shared dispatcher worker is a pinned actor16 with its own operating-system thread17. The dispatcher routes execution functions to those workers.
Creating six default actor systems in one process would therefore create twenty-four shared dispatcher worker threads before counting:
- additional domain dispatchers
- pinned application actors
- timeout timer threads
- scheduler threads
- remoting threads
Conclusion**
The actor system is a runtime root. Logical domains should normally be actor subtrees inside one actor system. Multiple actor systems remain valid for tests, migration, or a measured isolation need, but they are not the default composition primitive.
DONE Actor Trees and Contexts
Every actor created through ac:actor-of receives an actor context connected to the same actor system.
An actor can create children through its own context. Sento builds paths such as:
/user/domains /user/domains/cyber /user/domains/cyber/ingress
Actor names need to be unique only within the immediate context. Different domains can therefore use the same leaf name, such as ingress or collector, while retaining unique full paths.
Stopping an actor recursively stops its children first.
The actor context automatically watches actors it creates. When a child stops, the context removes that child from its actor list.
Conclusion**
Sento already supplies the tree required for domain and subdomain gservers. No separate hierarchy system is needed.
DONE Actor Failure Semantics
The actor-cell message handler catches serious-condition18. It logs the condition and returns:
(cons :handler-error condition)
The actor normally remains running.
The behavior applies to both synchronous and asynchronous actor handling because both eventually call the same handler path.
A watcher is not notified because the actor did not stop.
A pinned mailbox thread can die if a debugger abort restart unwinds past the handler. The mailbox implementation attempts to restart the pinned thread on the next submission.
Consequences**
- A supervisor cannot treat a handler error as a child crash.
- Watching is insufficient for failure supervision.
- A companion library cannot reliably add Erlang-style restart behavior19 without a core failure-policy hook.
- Current behavior should remain available for compatibility because existing code may expect handler errors as values.
Required core change**
Add actor failure policies:
- resume
- stop
- escalate
- custom classifier
Add a structured termination record with reason, condition, actor generation, failed-message summary, and timestamps.
DONE Lifecycle and Watching
Sento exposes:
pre-startafter-stop- constructor
:inithook - constructor
:destroyhook watchunwatchstop- context shutdown
- actor-system shutdown
A watcher currently receives:
(cons :stopped actor)
The notification contains no termination reason.
Stopping an actor:
- stops all children
- stops its mailbox
- runs the after-stop hook
- notifies watchers
Actor-system shutdown:
- stops the timeout timer
- stops the scheduler
- shuts down user actors
- shuts down internal actors
Consequences
- Destroy hooks cannot assume the scheduler remains available during actor-system shutdown.
- Declarative child startup order is absent.
- Restart intensity20, restart windows, restart backoff, and escalation are absent.
- Supervisor behavior must be added.
DONE Mailboxes and Queues
Sento has two mailbox types:
- pinned-thread mailbox
- dispatcher mailbox
Both use either a bounded or unbounded queue.
A bounded queue signals queue-full-error when capacity is reached.
The actor creation API accepts :queue-size. Zero or nil means unbounded.
Dispatcher mailbox behavior
A dispatcher mailbox stores messages in its own queue and submits one execution function to the dispatcher per message.
A mailbox lock prevents two dispatcher workers from changing one actor's state at the same time.
The source documentation states that processing order cannot be guaranteed when submissions arrive from multiple threads, although state access remains serialized.
Stop behavior
Stopping finishes the currently running message and discards queued messages. There is no queue-drain option.
Metrics
The queue protocol exports queued-count. The actor-to-mailbox-to-queue path and processed-message counts require internal slot access in existing benchmark code.
Consequences**
Required runtime additions:
- public mailbox depth and capacity
- processed, rejected, and dropped counts
- explicit overload policies
- structured overload result
- drain, discard, deadline, and handoff shutdown modes
- admission control21 at component ingress
DONE Dispatchers
A shared dispatcher contains:
- a router
- pinned worker actors
- one worker thread per routee
The actor system can register additional named dispatchers at startup or dynamically through register-new-dispatcher.
Actors select a dispatcher by identifier when created.
Tasks can also run on a selected custom dispatcher.
User decision recorded**
Dispatchers are per domain by default.
A domain may define subdomain dispatchers inside the same process.
Examples:
:domain/cyber/control :domain/cyber/default :domain/cyber/discovery :domain/cyber/scanning :domain/cyber/analysis
The domain control dispatcher must remain separate from saturated application work.
Consequences**
The Sento extension needs:
- domain dispatcher declarations
- subdomain dispatcher declarations
- thread-budget checks22
- dispatcher status and metrics
- application-work prohibition on control dispatchers
DONE Router Model
The current Sento router supports:
tellask-sask- random selection
- round-robin selection
- custom count-to-index selection
- routee addition
- listing routees
- stopping all routees
It does not support:
- routee removal
- routee replacement
- watching the router
- watching routees through the router
- message-aware strategy selection
- keyed affinity23
- dynamic pool sizing
- routee readiness
- routee health
- routee metrics
- lifecycle generation
Conclusion**
A raw Sento router is appropriate for simple static pools. A managed gserver needs a pool-manager actor plus router protocol extensions.
Required strategies:
The pool manager belongs on the domain control dispatcher. Routees belong on a domain or subdomain dispatcher.
DONE Event Streams
Sento creates a system-wide event stream by default.
The event-stream constructor can also create an event stream in any actor context. The source explicitly notes that a context-local event stream could address one actor hierarchy.
Subscriptions can match:
- all events
- type
- class
- symbol
- string
- list structure
The event stream stores subscriber references in a list. It does not watch subscribers or automatically remove a stopped subscriber.
Conclusion**
Each domain or subdomain may have its own scoped event stream without a separate actor system.
The managed event bus must:
- watch subscribers
- remove terminated subscribers
- expose subscriber metrics
- use typed events
- stop with the owning component
DONE Tasks, FSM, Stash, and Timers
Tasks
Sento tasks create temporary actor-backed operations on a selected dispatcher.
Task errors are converted into handler-error values.
Tasks are suitable for bounded short-lived work but do not replace managed routee pools for long-lived domain capacity.
FSM**
Sento FSM actors support named states, transitions, unhandled events, and state timeouts.
A supervisor may use FSM helpers, but the supervisor's child table, restart window, and lifecycle events must remain explicit data.
Stash**
The stash mixin stores deferred messages in an unbounded list. It supports tell and asynchronous ask patterns but not synchronous ask-s.
A supervisor should not use an unbounded stash as its primary overload strategy.
Timers**
Wheel-timer callbacks execute in the timer thread. Long-running callback work must be sent to actors or tasks.
The timeout timer is private actor-system infrastructure. Extensions currently access it through private symbols in remoting. A public scheduling interface is needed.
DONE Remoting Architecture
Sento remoting is a separate ASDF system26 depending on Sento, Bordeaux Threads, flexi-streams, pure-tls, usocket, and log4cl.
It includes:
- serializer protocol
- S-expression serializer
- network envelope
- TLS provider protocol
- pure-tls provider
- abstract transport protocol
- TCP transport
- connection pooling
- remote actor reference
- remoting context
- actor-path routing
The remoting state file reports sixty-two tests and one hundred sixty-two checks passing at the reviewed commit.
Transport
The TCP transport uses a four-byte length prefix and a default maximum message length of two megabytes.
It uses reader threads per connection and tracks accepted sockets for shutdown.
Remote references
A remote reference implements the actor tell, ask-s, and ask methods.
Each remote reference creates one local sender actor for queued tell messages.
Remote asks use correlation identifiers and a pending-ask table.
Inbound routing
Incoming target paths are resolved through find-actors. The first matching actor receives the deserialized message.
A missing tell target is logged and dropped.
A missing ask target is logged, but no structured not-found response is sent.
Inbound asks call the local actor with a fixed five-second timeout.
DONE Remoting Security Findings
Unsafe default reader setting
The default deserializer performs:
(read-from-string string)
It does not locally bind *read-eval* to nil.
Common Lisp reader evaluation27 can execute forms introduced through reader syntax when *read-eval* is true.
Immediate fix:
(let ((*read-eval* nil))
(read-from-string string))
This is necessary but not sufficient for a production untrusted-data serializer.
Arbitrary actor paths
Remoting exposes actor-system path lookup. There is no explicit exported-actor registry or capability check.
A production remote boundary must resolve only approved logical exports.
Optional TLS
The remoting API accepts a nil TLS configuration. Plain TCP is therefore possible.
Production mode must require TLS and verified peer identity28.
No actor authorization layer
The transport does not map an authenticated peer to allowed target actors or message types.
No deadline propagation
The envelope contains target, sender, payload, message type, and correlation identifier. It does not contain the caller deadline.
Error envelope not integrated
An error-envelope structure exists, but the inbound routing path does not use it for not-found, access-denied, overload, timeout, or handler failures.
Delivery semantics not explicit
Required remoting changes**
- disable reader evaluation
- add a schema serializer31
- require TLS in production mode
- add explicit actor exports
- map certificate identity to a principal32
- add authorization policy
- add per-peer quotas
- carry deadlines
- return structured errors
- document at-most-once behavior
- require idempotency keys33 for caller-enabled retries
- add remote-reference lifecycle and pending-ask limits
DONE CI and Test-Coverage Findings
The GitHub Actions workflow runs:
- Sento core tests
- Sento remoting tests
The active matrix currently lists only sbcl-bin on Ubuntu. Other Lisps and operating systems are commented out.
The workflow downloads Roswell with curl and clones remoting dependencies not present in Quicklisp.
Consequences**
The Sento extension should add:
- supervisor tests on shared and pinned dispatchers
- overload tests
- crash-loop tests
- routee replacement tests
- domain saturation tests
- subdomain saturation tests
- safe-deserializer tests
- actor-export denial tests
- deadline tests
- structured remote-error tests
- thread-leak tests
Portability claims must remain limited to the implementations actually tested.
DONE Architecture Revisit
The initial idea proposed six domain gservers and considered separate actor systems.
After source review, the corrected architecture is:
- One Sento actor system per process by default.
- One managed root component for domain composition.
- One supervisor per domain.
- Per-domain control and default dispatchers.
- Optional nested subdomain dispatchers inside each domain.
- Optional scoped event stream per domain or subdomain.
- Managed router pools for equivalent workers.
- Keyed routers for stateful affinity.
- Core Sento changes for failure policies and termination reasons.
- Companion Sento systems for supervision, components, managed routers, and observability.
- Harden existing Sento remoting for Sento-to-Sento links.
- Use a separate process when actual isolation or independent scaling is required.
DONE Core Versus Companion Boundary
| Capability | Existing Sento | Core change | Companion module |
|---|---|---|---|
| Actor tree | Yes | No | Component conventions |
| Named dispatchers | Yes | Metrics hooks | Domain dispatcher declaration |
| Handler error result | Yes | Failure policy | Supervisor consumes termination |
| Stop notification | Minimal | Structured termination | Restart strategy |
| Bounded queue | Yes | Overload and shutdown policy | Admission control |
| Static router | Yes | Routee removal and message-aware strategy | Managed pool actor |
| System event stream | Yes | Optional subscriber lifecycle hook | Scoped managed event bus |
| Metrics | Partial/internal | Public mailbox and dispatcher API | Aggregation and export |
| Remoting | Yes | Safe defaults and exported target protocol | Peer policy and component resolution |
| Component registry | No | No | Yes |
| Supervisor | No | Failure hooks required | Yes |
DONE Next Action
Implement Phase 0 and Phase 1 from SENTO-000 Managed Component Runtime:
- Pin the reviewed Sento commit.
- Reproduce core and remoting tests.
- Add failure-policy regression tests.
- Add structured termination records.
- Preserve
:resumeas compatibility behavior. - Prototype one-for-one supervision after termination events are reliable.
Footnotes and Glossary
DONE Sources
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/sento.asd
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/actor-system.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/actor-context.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/actor.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/actor-cell.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/mbox/message-box.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/queue/queue.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/dispatcher.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/router.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/eventstream.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/tasks.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/fsm.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/stash.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/remoting/remoting.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/remoting/remote-ref.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/remoting/serialization.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/specs/remoting-state.md
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/pull/112
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/pull/113
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/pull/114
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/pull/115
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/pull/116
Footnotes:
Sento: The Common Lisp actor framework in the mdbergmann/cl-gserver repository.
API: Application Programming Interface. The documented functions, classes, and data that one software component allows another component to use.
Companion library: A separate software package built on top of another library without placing all of its code inside the original core package.
Core: The central Sento runtime code that owns actor message handling, mailboxes, dispatchers, and lifecycle behavior.
Gserver: A managed service composed from one or more actors, dispatchers, routing, lifecycle state, and a stable entry point.
Dispatcher: A named pool of worker actors and threads that executes work taken from actor mailboxes.
Managed router: A router whose worker membership, failure handling, resizing, health, and metrics are controlled by a supervisor or pool manager.
Overload: A state where incoming work arrives faster than the system can process or store it safely.
Observability: Metrics, events, status, and traces that show what the runtime is doing.
Remoting: Sending actor messages between actor systems over a network.
Mailbox: The queue owned by one actor that stores messages waiting to be processed.
Actor system: The top-level Sento runtime container that owns actor contexts, dispatchers, timers, and the default event stream.
Actor context: A container that owns the immediate child actors beneath an actor or actor system.
Event stream: A publish-and-subscribe message bus used to distribute events to interested actors.
Wheel timer: A timer that stores future deadlines in rotating time slots.
Pinned actor: An actor whose mailbox owns one dedicated operating-system thread.
Thread: One independently scheduled execution path inside a process.
Serious condition: A Common Lisp condition representing an error or other event that interrupts normal processing.
Erlang-style restart behavior: Supervision behavior modeled after Erlang/OTP, where supervisors restart failed children according to declared strategies and limits.
Restart intensity: The maximum number of restarts permitted during a configured time window before a supervisor gives up or escalates.
Admission control: Accepting or rejecting work at a component entrance before it consumes deeper queues and workers.
Thread budget: The maximum number of operating-system threads a process is expected or allowed to create.
Keyed affinity: Routing related messages with the same key to the same worker so that worker can keep sequential state for that key.
Consistent hash: A routing method that minimizes how many keys move when workers are added or removed.
Rendezvous hash: A routing method that scores all workers for a key and chooses the highest score.
ASDF: Another System Definition Facility. The standard Common Lisp system and dependency definition tool.
Reader evaluation: Common Lisp reader behavior that can execute code while reading data when special syntax is present and *read-eval* is true.
Peer identity: The verified identity of the remote system connected to the local remoting endpoint.
Durable delivery: A message-delivery model that stores messages so they can survive temporary failure and be retried later.
At-most-once: A delivery model where a message is attempted no more than once, so it may be lost but is not automatically duplicated by retry.
Schema: A formal definition of allowed message fields, types, sizes, and structure.
Principal: The verified user, service, or node identity used in an authorization decision.
Idempotency key: A unique request value that allows a receiver to recognize a retried operation and avoid processing it twice.