☁ fluffly little clouds ☁
The back end for Unison Cloud
This repository contains the source for cloud-api, but the source for nimbus lives on Share.
When something changes that nimbus has cached — an environment losing access to a database, a service being (re)deployed or undeployed, an env var changing — cloud-api tells the nimbus nodes to invalidate the relevant cache. This is synchronous: the triggering request does not return until every node has acknowledged it applied the change (or a deadline trips), and each node's latency is measured so slow nodes are visible. (This replaced an older best-effort Consul Serf-event mechanism, which could silently drop or lag invalidations.)
Each nimbus node holds one persistent websocket to a single cloud-api instance (the
BYOC cluster protocol — see joinCluster). Nodes are sharded across cloud-api
instances, so no single instance can reach them all directly.
A change handler calls one of the fire*InvalidationEvent functions
(src/Cloud/Events.hs), which call invalidateSync
(src/Cloud/Web/Cluster/Impl.hs). invalidateSync:
- Local delivery (
invalidateClusterLocal): for every node connected to this instance, it stamps a correlation id (seq) on the event, pushes it over the websocket, and blocks until the node repliesInvalidationAck {seq, applyNanos}(or the ~30s deadline elapses). It records the cloud-api-measured round-trip and the node-reported apply time. - Peer fan-out: it discovers the other cloud-api instances from Consul (service
cloud-api-http, orcloud-api-staging-httpin staging — overridable with theCLOUD_API_CONSUL_SERVICEenv var) and calls each instance'sPOST /internal/invalidate, which runs the same local delivery there and returns its per-node results. - Aggregate: local + all peer results, deduped by node id (the discovered peer
list includes this instance, reached over loopback). Per-node latency is logged
(
rtt=… apply=…) — this is how you spot a slow node.
If no peers are discovered (e.g. local integration tests, where cloud-api isn't registered in Consul) it degrades to local-only delivery — correct for a single-instance deployment, where every node is attached to that instance.
Note: peer discovery returning an empty list is silent and looks identical to "single instance," so a wrong service name only manifests once more than one cloud-api instance is running. Watch the
cloud-api peer discovery … found N peer(s)log line.
The node's websocket receive loop applies the invalidation (clears the relevant
SemispaceCache — e.g. EnvironmentInvalidation drops the cached
environment→database mapping) and then replies with InvalidationAck carrying the
seq and the measured apply duration. The apply happens first; an ack failure is
logged, never fatal. The node source lives in the @cloud/nimbus project on Share.
A node advertises protocolVersion=2 when it joins. cloud-api stamps seq and blocks
on acks only for V2 nodes; older (V1) nodes get the legacy fire-and-forget push (no
ack). The seq field is optional on the wire, so it is backward compatible in both
directions and the two repos can deploy independently.
EnvironmentInvalidation, UserServiceInvalidation, ServiceIdInvalidation, and
ServiceHashInvalidation — all constructors of the Event type in
src/Cloud/Byoc/Env.hs, all flowing through the same invalidateSync path.
| Concern | File |
|---|---|
fire*InvalidationEvent entry points |
src/Cloud/Events.hs |
invalidateSync, local delivery, peer discovery, ack waiters |
src/Cloud/Web/Cluster/Impl.hs |
Internal peer RPC route (/internal/invalidate) |
src/Cloud/Web/Internal/API.hs, src/Cloud/Web/Internal/Impl.hs |
Event, NodeDelivery/PeerNodeResult, websocket Connection |
src/Cloud/Byoc/Env.hs |
See running-locally.md.
This will run integration tests against a local docker compose cluster:
make integration-tests-small