Workers
A worker in Substrate is a Kubernetes Pod that’s ready to host an actor.
It’s pre-warmed: the in-pod ateom helper is already running and the sandbox
binaries are staged on the node - so when ateapi assigns the worker, ateom can
create the sandbox and restore an actor into it without waiting on image pulls
or sandbox boot.
Every worker belongs to exactly one WorkerPool, and a pool is
single-class: either gVisor (ateom-gvisor + runsc) or micro-VM
(ateom-microvm + cloud-hypervisor). All pods in a pool run the same runtime,
and snapshots are not portable across classes.
Anatomy of a worker pod
flowchart LR
subgraph POD["Worker pod (idle: only the ateom container runs)"]
direction TB
OM["ateom<br/>(gvisor OR microvm)<br/>gRPC on Unix socket"]
SB[["Sandbox<br/>(gVisor sandbox via runsc,<br/>OR cloud-hypervisor micro-VM)<br/>created on assignment"]]
OM -- "create / restore" --> SB
SB -.->|hosts| WL[["Actor workload<br/>(after restore)"]]
end
AT[atelet on node] -- "Unix socket<br/>/var/lib/ateom-gvisor/<br/>ateoms/{uid}/ateom.sock" --> OM
click OM "/components/ateom-gvisor/" "ateom-gvisor"
click AT "/components/atelet/" "atelet"
The actor’s network is wired up on assignment by ateom - a per-activation veth pair into an interior netns, with nftables masquerade/DNAT in the pod netns. It’s torn down on checkpoint and rebuilt on restore. (Same model in both runtimes; see ateom-gvisor and ateom-microvm.)
What a worker is, in three sentences
- A Pod owned by a
WorkerPoolDeployment. - Running a single
ateomcontainer of its pool’s class (a pause process and the sandbox are created later, on assignment, not at pod start). - A row in Redis (
worker:<namespace>:<pool>:<pod>) once it has an IP, tracking whether it’s currently hosting an actor.
Worker states
Workers have a two-state lifecycle on the actor side: Idle or Assigned. See Worker lifecycle for the full state machine.
| State | Identifier |
|---|---|
| Idle | assignment == nil in the Redis worker record |
| Assigned | assignment set to an Assignment{actor_template, actor} |
A given worker hosts at most one actor at a time. There’s no multi-tenancy inside the sandbox.
The worker record
Each worker’s Redis record carries, among other fields:
assignment- anAssignment{actor_template: KubeNamespacedObjectRef, actor: ObjectRef};nilmeans idle. This replaces the old flatactor_id/actor_namespace/actor_templatefields, and scopes the binding by(atespace, actor_name).sandbox_class- the pool’s class (gvisorormicrovm), cached on the worker so scheduling can match it against an actor’s template class.node_name,labels- the node the pod landed on and the pool’s labels, both cached for eligibility and node-local snapshot placement.ip,worker_pod_uid- the pod IP and UID (the UID is the ateom socket address).
How a worker is created
You don’t create workers directly. You create a WorkerPool:
apiVersion: ate.dev/v1alpha1kind: WorkerPoolmetadata: { name: default }spec: replicas: 10 sandboxClass: gvisor # or microvm ateomImage: ghcr.io/.../ateom-gvisor:vX # sandboxConfigName: ... # else the cluster-default SandboxConfig for the classatecontroller turns this into a Deployment named <workerpool-name>
with replicas pods. As each pod gets a PodIP, ateapi inserts an idle worker
record into Redis (stamping in the pool’s sandbox_class, node_name, and
labels). Registration is keyed on the pod having an IP, not on the Ready
condition.
How a worker is assigned
When ResumeActor runs, ateapi’s worker-assignment step:
- Filters workers to those with no assignment that are eligible for
the actor - the worker’s
sandbox_classmust equal the template’s class, and the worker must satisfy both the template’sworkerSelectorand the actor’s ownworker_selector. - Honors node restrictions when the actor has node-local snapshots (a Pause prefers the node its snapshot lives on).
- Picks one at random (shuffles the eligible set) to spread load.
- Sets the worker’s
assignmentand stamps the worker’s pool + IP back onto the actor.
How a worker is released
After SuspendActor finishes the checkpoint and upload, ateapi clears the
worker’s assignment (and the actor’s worker_pool_name). The worker is
immediately eligible for the next actor.
What happens when a worker pod dies
When a worker pod is deleted, ateapi’s recovery path:
- If the worker had an
assignment, that actor is forced back to SUSPENDED (with whatever latest snapshot it already had). - The worker record is deleted from Redis.
This is a recovery path with a known race against concurrent SuspendActor calls.
Why two runtimes?
runsc (gVisor) gives a small-footprint userspace kernel with first-class
checkpoint/restore - the entire point of Substrate. The micro-VM class
trades that footprint for a hardware-virtualized boundary (a real guest kernel
behind KVM), suspending/resuming via cloud-hypervisor’s snapshot/restore
instead. A pool commits to one or the other.
What’s pre-warmed vs. lazy
| Pre-warmed (idle pod has it) | Lazy (only on assignment) |
|---|---|
| ateom running | Container image for the actor’s workload |
| Sandbox binaries cached on the node | OCI bundle built, actor network wired |
| Pod scheduled, IP, pod network ready | Sandbox created + actor state restored |
The first three are what makes resume sub-second despite the sandbox boundary.
Related
- Worker lifecycle - full state machine.
- WorkerPool - the CRD that makes workers exist.
- ateom-gvisor · ateom-microvm - the two runtimes that run inside.
- atelet - who drives them externally.