Working with Actor Ensembles
Ensemble Basics
The concept of the ensemble denotes the totality of actors running on a server substrate, perhaps in the Internet at large, and sharing a secret cookie that allows them to communicate with each other.
Each server has a server identifier, which is a symbol, that uniquely names the server within the ensemble. Each ensemble has a registry server, which plays a role similar to DNS resolvers in the Internet. It allows servers to find each other by using the server identifier, and implicit connect as needed without requiring the programmer or the operator to explicitly connect to each other.
By default, each server binds and listens to a UNIX domain socket
/tmp/ensemble/<server-identifier>
. Obviously you can specify
additional addresses, including Internet addresses where the server
should listen. If your ensemble spans multiple hosts, you should
specify the tcp addresses where your servers listen explicitly. If you
plan to expose your servers to the wider Internet, it is strongly
recommended to use TLS.
The gxensemble Management Tool
Gerbil comes with a powerful tool for managing actor ensembles: gerbil ensemble
or just gxensemble
.
Here are the commands it supports:
$ gxensemble help
gxensemble: the Gerbil Actor Ensemble Manager
Usage: gxensemble <command> command-arg ...
Commands:
run run a server in the ensemble
registry runs the ensemble registry
load loads code in a running server
eval evals code in a running server
repl provides a repl for a running server
ping pings a server or actor in the server
shutdown shuts down an actor, server, or the entire ensemble including the registry
list-servers lists known servers
list-actors list actors registered in a server
list-connections list a server's connection
lookup looks up a server by id or role
authorize authorize capabilities for a server
retract retract all capabilities granted to a server
cookie generate a new ensemble cookie
admin generate a new ensemble administrator key pair
help display help; help <command> for command help
Generating the ensemble cookie
Before starting an ensemble, we must generate a cookie for our servers
to authenticate each other. The cookie is placed in $GERBIL_PATH/ensemble/cookie
;
if you are running an ensemble spanning multiple hosts, you should copy the cookie
to the relevant hosts. Note that the tool will not overwrite an existing ensemble
cookie.
Here is the usage:
$ gxensemble help cookie
Usage: gxensemble cookie [command-option ...]
generate a new ensemble cookie
Command Options:
-f --force force the action
Generating and administrative key pair
If you want to limit administrative actions only to administrators,
you can generate an administraticve key pair with gxensemble admin
.
See Administrative Privileges below.
Here is the usage:
$ gxensemble help admin
Usage: gxensemble admin [command-option ...]
generate a new ensemble administrator key pair
Command Options:
-f --force force the action
Authorizing capabilities
This is an administrative action, that confers capabilities to an authorized server within the context of another server. See Administrative Privileges below.
Here is the usage:
$ gxensemble help authorize
Usage: gxensemble authorize [command-option ...] <server-id> <authorized-server-id> [<capabilities>]
authorize capabilities for a server
Command Options:
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
Arguments:
server-id the server id
authorized-server-id the server to authorize capabilities for
capabilities the server capabilities to authorize [default: (admin)]
Retracting capabilities
This is an administrative action, that retracts capabilities from a previously authorized server.
Here is the usage:
$ gxensemble help retract
Usage: gxensemble retract [command-option ...] <server-id> <authorized-server-id>
retract all capabilities granted to a server
Command Options:
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
Arguments:
server-id the server id
authorized-server-id the server to authorize capabilities for
Starting the ensemble
The first order of business when starting an actor ensemble, is to run a registry.
We can do this with the gxensemble registry
command:
$ gxensemble help registry
Usage: gxensemble registry [command-option ...]
runs the ensemble registry
Command Options:
--log <logging> specifies the log level to run with [default: INFO]
--log-file <logging-file> specifies a log file instead of logging to stderr; if it is - then the log will be written into the ensemble server directory log [default: #f]
-l --listen <listen> additional addresses to listen to; by default the server listens at unix /tmp/ensemble/<server-id> [default: ()]
-a --announce <announce> public addresses to announce to the registry; by default these are the listen addresses [default: #f]
Running an ensemble server
We can do this with the gxensemble run
command; it takes a module id
as an argument, loads it and executes the main
entry point with the
arguments passed in the command line.
Here is the usage of the tool:
Usage: gxensemble run [command-option ...] <server-id> <module-id> <main-args> ...
run a server in the ensemble
Command Options:
--log <logging> specifies the log level to run with [default: INFO]
--log-file <logging-file> specifies a log file instead of logging to stderr; if it is - then the log will be written into the ensemble server director log [default: #f]
-l --listen <listen> additional addresses to listen to; by default the server listens at unix /tmp/ensemble/<server-id> [default: ()]
-a --announce <announce> public addresses to announce to the registry; by default these are the listen addresses [default: #f]
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
--roles <roles> server role(s); a list of symbols [default: ()]
Arguments:
server-id the server id
module-id the module id
main-args arguments for the module's main procedure
Loading code
You can dynamically load code in any ensemble server using the
gxensemble load
command. This command will ship the object file
(and it is dependencies, unless forced by the user) to the remote
server and load them.
Here is the usage:
Usage: gxensemble load [command-option ...] <server-id> <module-id>
loads code in a running server
Command Options:
-f --force force the action
--library loads the code as library module; the library must be in the servers load path
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
--library-prefix <library-prefix> list of package prefixes to consider as library modules installed in the server [default: (gerbil scheme std)]
Arguments:
server-id the server id
module-id the module id
Evaluating code
You can evaluate an expression in a server using the gxensemble eval
command.
Note that the evaluator is the raw gambit evaluator, with no gerbil expansion.
$ gxensemble help eval
Usage: gxensemble eval [command-option ...] <server-id> <expr>
evals code in a running server
Command Options:
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
Arguments:
server-id the server id
expr the expression to eval
Getting a repl
The next step up from eval
is to get a repl on the running server; you can do this with the gxensemble repl
command.
Note that the repl does local expansion and remote evaluation; that means you can use the full gamut of gerbil code
Here is the usage of the command:
$ gxensemble help repl
Usage: gxensemble repl [command-option ...] <server-id>
provides a repl for a running server
Command Options:
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
--library-prefix <library-prefix> list of package prefixes to consider as library modules installed in the server [default: (gerbil scheme std)]
Arguments:
server-id the server id
The repl suppors a few control commands:
Control commands:
,(import module-id) -- import a module locally for expansion
,(load module-id) -- load the code and dependencies for a module
,(load -f module-id) -- forcibly load a module ignoring dependencies
,(load -l module-id) -- load a library module
,(defined? id) -- checks if an identifier is defined at the server
,(thread-state) -- display the thread state for the primordial thread group
,(thread-state -g) -- display the thread state for all thread groups recursively
,(thread-state sn) -- display the thread state for a thread or group identified by its serial number
,(thread-backtrace sn) -- display a backtrace for a thread identified by its serial number
,(shutdown) -- shut down the server and exit the repl
,q ,quit -- quit the repl
,h ,help -- display this help message
Ping a server or an actor
You can ping a server or an actor for liveness using the gxensemble ping
command:
$ gxensemble help ping
Usage: gxensemble ping [command-option ...] <server-id> [<actor-id>]
pings a server or actor in the server
Command Options:
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
Arguments:
server-id the server id
actor-id the actor's registered name [default: #f]
General Management Commands
The following commands are useful for general management tasks:
$ gxensemble help list-servers
Usage: gxensemble list-servers [command-option ...]
lists known servers
Command Options:
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
$ gxensemble help list-actors
Usage: gxensemble list-actors [command-option ...] <server-id>
list actors registered in a server
Command Options:
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
Arguments:
server-id the server id
$ gxensemble help list-connections
Usage: gxensemble list-connections [command-option ...] <server-id>
list a server's connection
Command Options:
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
Arguments:
server-id the server id
$ gxensemble help lookup
Usage: gxensemble lookup [command-option ...] <server-or-role>
looks up a server by id or role
Command Options:
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
--role lookup by role
Arguments:
server-or-role the server or role to lookup
$ gxensemble help shutdown
Usage: gxensemble shutdown [command-option ...] [<server-id>] [<actor-id>]
shuts down an actor, server, or the entire ensemble including the registry
Command Options:
-f --force force the action
--registry <registry> additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry [default: #f]
Arguments:
server-id the server id [default: #f]
actor-id the actor's registered name [default: #f]
A Working example: httpd with dynamic handler registration
We can make all this concrete with a working example: an httpd server ensemble, that supports dynamic handler registration.
The source code for the tutorial is available in the gerbil source distribution, in src/tutorial/ensemble. You can build it using the build script.
Firt let's look at the server implementation in src/tutorial/ensemble/server.ss:
(import :gerbil/gambit/threads
:std/net/httpd
:std/io)
(export run!)
(def (run! port)
(let* ((addr (cons inaddr-any4 port))
(httpd (start-http-server! addr)))
(thread-join! httpd)))
This is a very thin server, that gets started with the run!
method
of the module; it takes a port, starts an httpd server and waits for
it to shutdown. Notice that the server is initialized with the
default mux, and it is initially empty; there are no handlers, so
everything will 404.
We also have a server wrapper for running the server as a service with gxensemble run
.
This is the code for the wrapper (see src/tutorial/ensemble/httpd-svc.ss):
(import ./server)
(export main)
(def (main (port "8080"))
(run! (string->number port)))
With all this, let's start an ensemble with two httpds, named httpd1
and httpd2
:
# generate a cookie for our ensemble, if on does not already exist
$ gxensemble cookie
# start the registry
$ gxensemble registry
...
# start the servers
$ gxensemble run --roles "(httpd)" httpd1 :tutorial/ensemble/httpd-svc 8080
...
$ gxensemble run --roles "(httpd)" httpd2 :tutorial/ensemble/httpd-svc 8081
...
Now let's look at our servers:
$ gxensemble lookup --role httpd
(httpd1 (unix: "dellicious" "/tmp/ensemble/httpd1"))
(httpd2 (unix: "dellicious" "/tmp/ensemble/httpd2"))
We can also ping them for liveness:
$ gxensemble ping httpd1
OK
$ gxensemble ping httpd2
OK
Now if we do a request at them we will see that there are no handlers:
$ curl -I http://localhost:8080/
HTTP/1.1 404 Not Found
Date: Tue Aug 15 06:34:30 2023
Content-Length: 0
$ curl -I http://localhost:8081/
HTTP/1.1 404 Not Found
Date: Tue Aug 15 06:34:43 2023
Content-Length: 0
At the next step, we can load some code that implements handlers for our httpds; the code with the handlers lives in src/tutorial/ensemble/handler.ss:
(import :std/net/httpd)
(export #t)
(def greeting
"hello there!\n")
(def (set-greeting! what)
(set! greeting what))
(def (write-simple-handler req res)
(http-response-write res 200 '(("Content-Type" . "text/plain"))
greeting))
(def (write-chunked-handler req res)
(http-response-begin res 200 '(("Content-Type" . "text/plain")))
(http-response-chunk res "hello ")
(http-response-chunk res "there!\n")
(http-response-end res))
(def (root-handler req res)
(http-response-write res 200 [] "the world is not flat but round!\n"))
(http-register-handler (current-http-server) "/" root-handler)
This code registers a root handler, and provides two more handlers that are not initially registerd anywhere.
Here is how we can load the code:
# load with -f as there is no need to load any library dependencies
$ gxensemble load -f httpd1 :tutorial/ensemble/handler
... loading code object file /home/vyzo/.gerbil/lib/tutorial/ensemble/handler__0.o3
ca3f193373a296d7bdb9101e7d4b9f1d450676aec6c49f05202a3dbcc5d766e2
$ gxensemble load -f httpd2 :tutorial/ensemble/handler
... loading code object file /home/vyzo/.gerbil/lib/tutorial/ensemble/handler__0.o3
ca3f193373a296d7bdb9101e7d4b9f1d450676aec6c49f05202a3dbcc5d766e2
and we can verify that the two servers now have a root handler:
$ curl http://localhost:8080/
the world is not flat but round!
$ curl http://localhost:8081/
the world is not flat but round!
Finally, we can use the repl to install another handler from the module we just loaded:
$ gxensemble repl httpd1
httpd1> ,(import :tutorial/ensemble/handler)
httpd1> ,(import :std/net/httpd)
httpd1> (set-greeting! "hello, i am httpd1\n")
httpd1> (http-register-handler (current-http-server) "/greeting" write-simple-handler)
httpd1> ,q
$ curl http://localhost:8080/greeting
hello, i am httpd1
Working with binary executables
So far we have demonstrated ensembles with dynamic executable modules; in
practice however, you are most likely to ship a binary executable to your
server. Of course this is not a problem; all you have to do is run your
server's entry point using call-with-ensemble-server
; this is what
gxensemble run
does after all. The only difference is that you will
have to parse CLI options on your own, probably using getopt.
Note that some care should be taken to ensure necessary bindings are available in the server and not eliminated by the tree shaker from full program optimization. As such, it is strongly recommended that you do not use full program optimization for executable binaries compiled for servers; otherwise it is very much likely that some essential bindings will be missing, causing your server to crash when trying to load code.
Here is an example binary executable running our httpd; the code is at src/tutorial/ensemble/httpd-exe.ss:
(import :std/actor
./server)
(export main)
(def (main server-id (port "8080"))
(let ((port (string->number port))
(server-id (string->symbol server-id)))
(call-with-ensemble-server
server-id (cut run! port)
roles: '(httpd))))
And here it running and getting managed with gxensemble
:
$ httpd-exe httpd3 8082
...
$ gxensemble lookup --role httpd
(httpd1 (unix: "dellicious" "/tmp/ensemble/httpd1"))
(httpd2 (unix: "dellicious" "/tmp/ensemble/httpd2"))
(httpd3 (unix: "dellicious" "/tmp/ensemble/httpd3"))
$ gxensemble repl httpd3
httpd3> ,(load :tutorial/ensemble/handler) ; load the code in the remote server
httpd3> ,(import :tutorial/ensemble/handler) ; import for local expansion
httpd3> (set-greeting! "hello, i am httpd3 and i am a binary executable\n")
httpd3> ,(import :std/net/httpd) ; import for local expansion
httpd3> (http-register-handler (current-http-server) "/greeting" write-simple-handler)
httpd3> ,q
$ curl http://localhost:8082/greeting
hello, i am httpd3 and i am a binary executable
Shutting down
At this point, we are done with this tutorial, and we can shutdown our ensemble:
$ gxensemble shutdown -f
... shutting down httpd1
... shutting down httpd2
... shutting down httpd3
... shutting down registry
Administrative Privileges
You may have noticed that the gxensemble
tool has some powerful and
potentially destructive capabilities. In general, this is fine for
development or when your ensemble is limited to a single host, but as
your system grows and spans more hosts and involves more people, it
might be prudent to limit administrative capabilities to authorized
administrators.
The actor system in Gerbil allows you to (optionally) use a Ed25519 key pair that limits administrative actions (shutdown, code loading and evaluation, etc) only to entities that can prove that they have access to the private key material.
This is integrated with the gxensemble
tool:
- You can generate an administrative key pair with the
gxensemble admin
command. The command will ask for a passphrase to encrypt the private key, and will leave the key pair in$GERBIL_PATH/ensemble/admin.{pub,priv}
. - Subsequently, when attempting a senstive action that requires administrative privileges the tool will ask you to enter the passphrase in order to unlock and use the private key to elevate privileges in the servers involved.
Furthermore, using the administrative key pair, you can confer
capabilities to servers, within the the context of another server.
For example, you can confer the shutdown
capability to another
server within the context of server.
For example, to allow actors in my-authorized-server
to shutdown
my-server
, you can issue the following command with administrative
privileges:
$ gxensemble authorize my-server my-authorized-server "(shutdown)"
You can retract capabilities from a server with the retract
command
of the gxensemble
tool:
$ gxensemble retract my-server my-authorized-server
In order to effectively and securely confer capabilities to other servers by name, it is strongly recommended that you use TLS.
Otherwise anyone in the ensemble can claim your authorized server's id and acquire capabilities that are not intended.