Because there are a number of different Syndicate implementations, and they all need to interoperate, I’ve used
Preserves Schema1 to define message formats
that all the implementations can share.
You can find the common protocol definitions in a new git repository:
I used
git-subtree
to carve out a
portion of the novy-syndicate source tree
to form the new repository. It seems to be working well. In the
projects that depend on the protocol definitions, I run the following
every now and then to sync up with the syndicate-protocols
repository:
1
2
3
4
git subtree pull -P protocols \
-m 'Merge latest changes from the syndicate-protocols repository' \
git@git.syndicate-lang.org:syndicate-lang/syndicate-protocols \
main
Previously, the
mini-syndicate
package for Python 3 implemented an older version of the Syndicate
network protocol.
A couple of weeks ago, I dusted it off, updated it to the new
capability-oriented Syndicate protocol, and fleshed out its nascent
Syndicated Actor Model code to be a full implementation of the model,
including capabilities, object references, actors, facets, assertions
and so on.
The new implementation makes heavy use of Python decorators to work
around Python’s limited lambda forms and its poor support for
syntactic extensibility. The result is surprisingly not terrible!
The revised codebase is different enough to the previous one that it
deserves its own new git repository:
It’s also available on pypi.org, as package
syndicate-py.
Updated Preserves for Python
As part of the work, I updated the Python Preserves implementation
(for both python 2 and python 3) to include the text-based Preserves
syntax as well as the binary syntax, plus implementations of Preserves
Schema and Preserves Path. Version 0.7.0 or newer of the
preserves package on pypi.org
has the new features.
For my system layer project, I need a
fast, low-RAM-usage, flexible, extensible daemon that speaks the
Syndicate network protocol and
exposes dataspace and system management services to other processes.
It uses the Syndicated Actor Model internally to structure its
concurrent activities. The actor model implementation is split out
into a crate of its own,
syndicate, that can be used by
other programs. There is also a crate of macros,
syndicate-macros, that
makes working with dataspace patterns over Preserves values a bit
easier.1
A gatekeeper that resolves long-lived,
“sturdy” references
like <ref "syndicate" [] #[acowDB2/oI+6aSEC3YIxGg==]>
to on-the-wire usable object capability references;
An inotify-based configuration watcher service that monitors a
directory full of Preserves text files for changes;
Future work for syndicate-macros is to
add syntactic constructs for easily establishing handlers for
responding to assertions and messages, cutting out the
boilerplate, in the same way that
Racket’s syndicate macros
do. In particular, having a during! macro would be very useful. ↩
As part of my other implementation efforts, I made enough improvements
to the Rust Preserves implementation to warrant releasing version
1.0.0.
This release supports the Preserves data model and the binary and text
codecs. It also includes a Preserves Schema compiler for Rust (for use
e.g. in build.rs) and an implementation of Preserves Path.
There are four crates:
preserves, Rust
representations of
Values
plus utilities and binary and text codecs;
At the beginning of August, I designed a query language for Preserves
documents inspired by
XPath.
Here’s the draft specification.
To give just a taste of the language, here are a couple of example
selectors:
🙂 Really! The Syndicate network protocol is simple, easily within reach of a
shell script plus e.g. netcat or openssl.
Here’s the code so far.
The heart of it is about 90 SLOC, showing how easy it can be to
interoperate with a Syndicate ecosystem.
Instructions are in
the README,
if you’d like to try it out!
The code so far contains functions to interact with a Syndicate server
along with a small demo, which implements an interactive chat program
with presence notifications.
Because Syndicate’s network protocol is polyglot, and the bash demo
uses
generic chat assertions,
the demo automatically interoperates with other implementations, such
as the analogous python chat demo.
The next step would be to factor out the protocol implementation from
the demo and come up with a simple make install step to make it
available for system scripting.
I actually have a real use for this: it’ll be convenient for
implementing simple system monitoring services as part of a
Syndicate-based system layer. Little bash
script services could easily publish the battery charge level, screen
brightness level, WiFi connection status, etc. etc., by reading files
from /sys and publishing them to the system-wide Syndicate server
using this library.
One promising application of dataspaces is dependency tracking for
orderly service startup.
The problem of service startup appears at all scales. It could be
services cooperating within a single program and process; services
cooperating as separate processes on a single machine; containers
running in a distributed system; or some combination of them all.
Syndicate programs are composed of multiple services running together,
with dependencies on each other, so it makes sense to express service
dependency tracking and startup within the programming language.
In the following, I’ll sketch service dependency support for
cooperating modules within a single program and process. The same
pattern can be used in larger systems; the only essential differences
are the service names and the procedures for loading and starting
services.
A scenario
Let’s imagine we have the following situation:
A program we are writing depends on the “tcp” service, which in turn
depends on the “stream” service. Separately, the top-level program
depends on the “timer” service.
An asserted RequireService record indicates demand for a running
instance of the named service; an asserted ServiceRunning record
indicates presence of the same; and interest in a ServiceRunningimplies assertion of a RequireService.
A library “service manager” process, started alongside the top level
program, translates observed interest in ServiceRunning into
RequireService, and then translates observed RequireService
assertions into service startup actions and provision of matching
ServiceRunning assertions.
1
2
3
4
5
6
(during(Observe(:pattern(ServiceRunning,(DLit$service-name)))_)(assert(RequireServiceservice-name)))(during/spawn(RequireService$service-name);; ... code to load and start the named service ...)
Putting these pieces together, we can write a program that waits for a
service called 'some-service-name to be running as follows:
1
(during(ServiceRunning'some-service-name)...)
When the service appears, the facet in the ellipsis will be started,
and if the service crashes, the facet will be stopped (and restarted
if the service is restarted).
Services can wait for their own dependencies, of course. This
automatically gives a topologically sorted startup order.
Modules as services, and macros for declaring dependencies
First, services can be required using a with-services macro:
1
2
3
4
(with-services[syndicate/drivers/tcpsyndicate/drivers/timer];; ... body expressions ...)
Second, each Racket module can offer a service named after the
module by using a provide-service macro at module toplevel. For
example, in the syndicate/drivers/tcp Racket module, we find the
following form:
1
2
3
4
5
(provide-service[ds](with-services[syndicate/drivers/stream](atds;; ... set up tcp driver subscriptions ...)))
Finally, the main entry point to a Syndicate/rkt program can use a
standard-actor-system macro to arrange for the startup of the
“service manager” process and a few of the most frequently-used
library services:
1
2
3
4
(standard-actor-system[ds];; ... code making use of a pre-made dataspace (ds) and;; preloaded standard services ...)
This past week I have been dusting off my old implementation of the SSH protocol in order to
exercise the new Syndicated Actor design and implementations. You can
find the new SSH code here.
(You’ll need the latest Syndicate/rkt
implementation to run it.)
The old SSH code used a 2014 dataspace-like language dialect called
“Marketplace”, which shared some concepts with modern Syndicate but
relied much more heavily on a pure-functional programming style within
each actor. The new code uses mutability within each actor where it
makes sense, and takes advantage of Syndicate DSL features like facets
and dataflow variables that I didn’t have back in 2014 to make the
code more modular and easier to read and work with.
plus a couple of small Syndicate/rkt syntax changes (renaming when
to on, making the notion of “event expander” actually useful, and a
new once form for convenient definition of state-machine-like
behaviour).
I’ve been fleshing out the syndicate-rkt Racket implementation based
on the novy-syndicate TypeScript sketch. I just reached a milestone
of TCP-based interoperability between the two implementations (yay!),
but there’s an interesting little side track involved that I thought
I’d write about.
The novy-syndicate code had a placeholder “dataspace” implementation
that had extremely limited pattern-matching. It was only able to
offer subscribers the ability to select (1) record assertions having
(2) a user-selected, constant label.
For example, a subscriber could elect to receive all records labelled
with Present; or with Says. Subscribers were not able to even
specify arity of matched records. It really was a placeholder for a
proper implementation to come later (ported across from a previous
syndicate/js implementation).
By contrast, the syndicate-rkt code has a full-fledged dataspace
able to index assertions according to quite sophisticated patterns:
Now, I managed to get the novy-syndicate example programs to talk to
the full syndicate/rkt dataspace - without changing the code!
The way I did it was to rewrite assertions travelling between the
programs on the fly.
And the way I did that was to include “rewrite” statements in the
capability I gave to the novy-syndicate client to allow it to
connect to the syndicate/rkt server.
The idea was to rewrite assertions-of-interest (subscriptions) from
the simple label-only pattern of novy-syndicate to the equivalent
full-dataspace pattern of syndicate/rkt, and to rewrite the
responses from the dataspace from the arbitrary-arity responses of
syndicate/rkt to the simple unary responses of novy-syndicate.
Here’s the rewrite specification,1 which
ultimately appears embedded as a “caveat” inside the Macaroon-style
capabilities that Syndicate uses:2
try matching <ObserveL C>, where L is a symbol and
C an embedded capability; if it does not match, skip the
remainder of this step; otherwise, rewrite it into <Observe
<bind assertion ⌜P⌝>f(C)>, where
P is a pattern matching records of the form <L_>, and
the quotation operator ⌜·⌝ quotes a pattern over assertions
into a term conforming to the Pattern schema above; and
f(C) “attenuates” C by attaching rewrites to it. Any
assertion sent to C is required to be of the form [V],
and is rewritten into just V.
if the rewrite in step 1 didn’t apply then match anything; call it
n; and rewrite it to itself.
The net effect is that when the simple chat example from
novy-syndicate asserts
<Observe Present #!C>
the syndicate-rkt server actually sees
<Observe <bind assertion ⌜<Present _>⌝> #!f(C)>
and when syndicate-rkt replies with an actual concrete presence
record, for example3
[<Present "Tony">]
the novy-syndicate client will actually receive just
<Present "Tony">
Cool huh?
Now, this works great for Present, which is unary, but not so well
for the client’s subscription to Says, which is binary: <Sayswho
what>. So our interoperability is limited here: the client only
sees presence information from its peers, and the actual utterances
sent get dropped on the floor for lack of an appropriate pattern at
the syndicate-rkt dataspace. To fix this, we could include a more
complex rewrite specification that treated Presence and Says
subscriptions separately and explicitly, with the correct arity for
each. But I’m done for now, and will focus on getting a proper
dataspace implementation into novy-syndicate instead.
We need a DSL for these rewrite
specifications! I’m working on it. It’ll probably look like the
existing Syndicate DSL syntax for patterns. ↩
Here’s the whole capability, including an
“oid” identifying the service to be accessed, the sequence of
“caveats” rewriting and attenuating information flowing through
the capability, and the signature proving the capability’s
validity:
The single-element list is there because the
rewritten pattern included a single binding named assertion, so
there’s a single value in the list of potentially-many values sent
back to the subscriber. The simplified novy-syndicate patterns
included exactly one implicit whole-assertion binding, and so the
list wrapper is also implicit in the novy-syndicate variation,
which is why it has to be explicitly removed to get
interoperability here. ↩