WorkerPool
WorkerPool is one of Substrate’s two CRDs. It says: “I want N pre-warmed
worker pods running this ateom-gvisor image, ready to host actors of
templates that target this pool.”
The spec
apiVersion: ate.dev/v1alpha1kind: WorkerPoolmetadata: name: defaultspec: replicas: 10 ateomImage: ghcr.io/agent-substrate/ateom-gvisor:vXstatus: replicas: 10 # actual countpkg/api/v1alpha1/workerpool_types.go
A real example: kagent-default
Here’s the live WorkerPool from the kagent namespace - installed by
the kagent Helm chart and used by every SandboxAgent running on
substrate today, including the hello-substrate
ActorTemplate:
apiVersion: ate.dev/v1alpha1kind: WorkerPoolmetadata: name: kagent-default namespace: kagent labels: app.kubernetes.io/instance: kagent app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: kagent app.kubernetes.io/part-of: kagentspec: ateomImage: localhost:5001/ateom-gvisor:latest replicas: 2status: replicas: 2The whole spec is two fields. atecontroller’s WorkerPool reconciler
turned that into a Deployment named kagent-default-deployment (the
-deployment suffix is hard-coded by the reconciler):
$ kubectl get deploy -n kagent kagent-default-deploymentNAME READY UP-TO-DATE AVAILABLE AGEkagent-default-deployment 2/2 2 2 11h
$ kubectl get pods -n kagent -l ate.dev/worker-pool=kagent-defaultNAME READY STATUS RESTARTS AGEkagent-default-deployment-7f464cd64c-m2fps 1/1 Running 0 11hkagent-default-deployment-7f464cd64c-r6lhv 1/1 Running 0 11hA few things worth pointing out:
ateomImageis the gVisor-hosting pod image, not the agent image. Every pod in this Deployment runsateom-gvisor, which is the per-node manager that boots runsc sandboxes for actors. Actor containers (e.g. thegolang-adkimage fromhello-substrate) live inside runsc, not in this pod’s container spec. See ateom-gvisor.replicas: 2is the whole capacity knob. This cluster can host exactly two actors concurrently per template-pool binding. Bump it and atecontroller scales the Deployment; new pods register themselves withWorkerPoolSyncerand Redis without restarting anything.- The pods are
privilegedand mount/var/lib/ateom-gvisorfrom the host. That’s how ateom-gvisor manages runsc state across the pod boundary - actors that survive a pod restart do so via files on the host path, not in the pod’s ephemeral storage. - The Deployment selector is
ate.dev/worker-pool=kagent-default. ateapi uses this label to find pool members when reconciling Redis-side worker records. - Owner-referenced by the WorkerPool. Delete the
WorkerPoolCRD and the Deployment (and all its pods) go away.
What atecontroller does with it
flowchart LR
WP[WorkerPool CRD] --> CTR[atecontroller<br/>WorkerPool reconciler]
CTR -- SSA apply --> D["K8s Deployment<br/>{name}-deployment"]
D --> P1[pod 1]
D --> P2[pod 2]
D --> PN[...]
P1 -- Ready --> S[ateapi<br/>WorkerPoolSyncer]
P2 -- Ready --> S
PN -- Ready --> S
S --> R[(Redis: idle worker records)]
- Reconciler reads
spec.replicasandspec.ateomImage. - Server-side-applies a Deployment named
<workerpool-name>-deploymentwith that replica count and image. - Updates
status.replicaswith what K8s reports.
That’s the whole reconciler. It doesn’t insert worker records into
Redis - that happens later via ateapi’s WorkerPoolSyncer, which
watches the pods directly.
internal/controllers/workerpool_controller.go:52-176
Isolation boundary
ActorTemplate.spec.workerPoolRef pins each template to a specific pool.
Different pools never share workers. Use this to:
- Run actors with different sandbox images.
- Scale capacity for different workload classes independently.
- Isolate failure domains.
Scaling
Edit spec.replicas, apply, done. atecontroller updates the Deployment,
new pods come up, the syncer registers them. No actor downtime.
Scaling down: terminated pods that were hosting actors trigger the
“worker died” recovery path - actors are reset to SUSPENDED from their
LastSnapshot.
Related
- Worker - what a pool member actually is.
- Workers component - pod-level detail.
- Worker lifecycle - the state machine.
- atecontroller - the reconciler.