Running a remote cluster
This example runs a cluster using the remote topology: a standalone runtime owns the data store and coordinates placement, state, and alarms, while two stateless workers connect to it and host the actors.
It mirrors the examples/remote-worker
example in the repository. The actor code is identical to the local cluster
example — only the host setup differs.
What you’ll run#
- One runtime (the
cmd/runtimebinary) that owns a SQLite data store. - Two workers (
host/remote) that connect to the runtime, each serving the actor typemyactorand exposing an HTTP control server.
Configuring the runtime#
The runtime is configured with a YAML file:
# config.yaml
bind: "127.0.0.1:7400"
# Runtime PSKs derive the cluster CA — keep them secret, inject from the environment in production
runtimePSKs:
- "example-runtime-psk-change-me"
# How joining hosts authenticate
# This example uses a shared host PSK; the workers must present the same value
bootstrap:
method: psk
hostPSK: "example-host-bootstrap-psk-change-me"
provider:
type: sqlite
connectionString: "data.db"
log:
level: debugBuild the runtime binary and print the cluster CA so the workers can pin it:
go build -o bin/runtime github.com/italypaleale/francis/cmd/runtime
# Print the CA so workers can verify the runtime on first connection
bin/runtime print-ca -config config.yamlCreating a remote worker#
A remote worker is a host/remote host. Unlike a local host, it doesn’t embed a data store — it connects to the runtime:
import (
"time"
"github.com/italypaleale/francis/host/remote"
)
// Shared secret the host proves to the runtime when it first joins
// Must match the runtime's bootstrap.hostPSK
const hostBootstrapPSK = "example-host-bootstrap-psk-change-me"
func runWorker(ctx context.Context) error {
h, err := remote.NewHost(
remote.WithAddress(actorHostAddress), // peer address for host-to-host invocations
remote.WithRuntimeAddresses(runtimeAddress), // one or more runtime replicas
remote.WithHostBootstrapPSK([]byte(hostBootstrapPSK)),
remote.WithPinnedCA(caPEM), // pin the CA from "runtime print-ca"
remote.WithShutdownGracePeriod(10 * time.Second),
)
if err != nil {
return err
}
err = h.RegisterActor("myactor", NewMyActor, remote.RegisterActorOptions{
IdleTimeout: 10 * time.Second,
})
if err != nil {
return err
}
service := h.Service()
_ = service
return h.Run(ctx)
}The repository example uses
remote.WithUnsafeNoPinnedCA()to trust the runtime on first connection for simplicity. In production, pin the CA withremote.WithPinnedCAinstead — see Security .
Running everything#
Start the runtime, then two workers:
# Terminal 1 — the runtime
go run github.com/italypaleale/francis/cmd/runtime -config config.yaml
# Terminal 2 — worker 1
go run /path/to/remote-worker -worker-address 127.0.0.1:8081 -actor-host-address 127.0.0.1:7571 -runtime-address 127.0.0.1:7400
# Terminal 3 — worker 2
go run /path/to/remote-worker -worker-address 127.0.0.1:8082 -actor-host-address 127.0.0.1:7572 -runtime-address 127.0.0.1:7400Invoking actors#
The control API is the same as the local example — invoke through either worker:
curl -X POST http://localhost:8081/invoke/myactor/id1/increment --data '{"In": 42}'
curl -X POST http://localhost:8082/invoke/myactor/id2/increment --data '{"In": 42}'The runtime decides which worker hosts each actor, so calls for the same ID always land on the same activation regardless of which worker you call.
State survives restarts#
Because the runtime persists state to SQLite (data.db), actor state outlives both the workers and the runtime. Invoke an actor a few times to build up its counter, restart the runtime, then invoke again — the counter continues from where it was.
Full source#
See examples/remote-worker
in the repository for the complete, runnable code, including a supervisord configuration that launches the runtime and both workers together.