atelet: actor state directories are never reclaimed when a worker pod goes away
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 38/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- go, kubernetes
- Domain
- backend, distributed-systems, infrastructure
Research direction
Start by tracing WorkerPoolSyncer.reconcile through WorkerWorkflow.ensureBoundActorsReleased and ActorWorkflow.ensureAteletTerminated, then inspect cmd/atelet/ateomgc.go and the imagegc.go pattern. Done means actor directories are reclaimed on worker deletion and by a fail-closed sweep, while vanished ateoms and local pause snapshots are handled as described without blocking deregistration.
Written by the indexing model from the issue text.
Description
What happened?
Nothing removes /var/lib/ateom-gvisor/actors/<actor-UID>/ when the worker pod hosting the actor goes away. The directory holds the actor's durable-dir contents, checkpoint/restore state and bundle overlay — for a durable-dir workload that is gigabytes per actor, not the few idle bytes of #1677's socket dirs. The directories persist for the life of the node.
This surfaced as node disk climbing during a durable-dir benchmark and never falling afterwards, eventually reaching ~90% and getting atelet evicted.
Measured on a 3-node dev cluster (c3-standard-4, 100 GB pd-balanced) against a0d306ee. Workload: 15 concurrent clients, each driving an actor that writes a 1 GiB file into its durable dir and then suspends and resumes repeatedly, for 10 minutes.
| Metric | Measurement / Impact |
|---|---|
| orphaned actor dirs | 23 (19 with payload, 4 empty template actors) |
retained under actors/ |
27.10 GB |
| per actor | ~1.4 GB |
| reclaimed by scaling the WorkerPool to 0 | 0 bytes |
reclaimed by --delete-all (full uninstall) |
0 bytes |
| reclaimed by a subsequent reinstall | 0 bytes — fresh atelets start on top of it |
Reproduced on both sandbox classes; they share ateompath.BasePath, so the layout and the leak are identical. An equivalent micro-VM run left 18.33 GB across 16 dirs, likewise unchanged by scaling to 0.
[!NOTE]
#1654 ("Remove an actor's directory when atelet terminates it") does not cover this. All figures above are from a build containing it. It removes the directory whenTerminateruns; the defect is thatTerminateis never reached once the pod is gone.
No delete is required to trigger this — an actor being de-scheduled from a node is enough.
Mechanism
As far as I can tell, two independent gaps compose:
-
The worker-delete path never contacts the node.
WorkerPoolSyncer.reconcilenotices the pod is gone and calls Control APIDeleteWorker.WorkerWorkflow.ensureBoundActorsReleased→releaseBoundActormoves each bound actor toCRASHEDand clears its pod pointers, but nothing asks atelet to terminate it. After the worker record is deleted, the actor names no worker, no worker names a node, and nothing ever revisits the actor UID — the directory is orphaned permanently. -
The actor-delete path gives up when the pod is gone.
ActorWorkflow.ensureAteletTerminateddialsDialForWorker(ns, pod)and returns nil onErrWorkerPodNotFound. That is the correct shortcut for a pod that legitimately vanished, but it is also exactly how the state comes to be orphaned.
Even when a Terminate is issued, it fails before reaching the directory cleanup: the teardown dials the ateom socket first, and a vanished ateom (connect: no such file or directory, connection refused) aborts the call ahead of the unmounts and the directory removal.
Expected Behavior
Actor node state is reclaimed once no actor can use it again. Two complementary mechanisms, mirroring the split proposed in #1677:
-
Prompt reclaim on the delete path. The worker-delete workflow is the last moment anything still knows which node holds the state, so it should tell that node to reclaim before releasing the record. Reaching the atelet requires dialing by node rather than by pod, since the pod is precisely what has disappeared. This must be best-effort: an unreachable node cannot be allowed to wedge worker deregistration.
Terminatealso needs to tolerate a vanished ateom — skip the sandbox teardown and still run the unmounts and directory cleanup — otherwise the call fails before doing the part that matters. A reachable ateom that rejects the call should still fail: that sandbox is live. -
A sweep as backstop. Evictions, node crashes and uninstalls produce orphans that no delete path will ever visit. A periodic atelet sweep reconciling
actors/against the actors the control plane places on that node covers them. It must fail closed — if the live set cannot be read in full, delete nothing — and must skip actors holding a local pause snapshot, which is node-pinned state whose deletion is unrecoverable.
Neither covers the graceful path. Reclaiming during Checkpoint as well would keep steady state near zero and shrink the sweep's caseload; that is a separate change.
Steps to Reproduce
- Install ate-system and deploy a WorkerPool with 15 workers.
- Record the node state:
du -sb /var/lib/ateom-gvisor/* && ls -1 /var/lib/ateom-gvisor/actors | wc -l - Drive 15 concurrent actors for ~10 minutes, each writing a 1 GiB file into its durable dir and suspending/resuming between iterations.
- Record the node state again — expect several GB per node under
actors/. - Scale the WorkerPool to 0, wait for the pods to drain, and record again.
Every actor directory and every byte is still present. The same holds after hack/install-ate.sh --delete-all and after a fresh reinstall.
Sandbox Runtime
Both / Runtime Agnostic — gVisor and micro-VM share ateompath.BasePath.
Agent Substrate Version / Commit SHA
main @ a0d306ee
Additional Context
Not fixed by #1678. That PR adds defer os.RemoveAll(ateomDir) to the two ateom runtime mains, which covers ateoms/<pod-UID>/ only. actors/<uid>/ is written and owned by atelet, not by ateom, and is not touched by that defer — nor could it be, since the ungraceful exits that strand actor state are exactly the cases where a defer does not run. Same pattern, different directory, very different blast radius:
| #1677 / #1678 | this issue | |
|---|---|---|
| path | ateoms/<worker-pod-UID>/ |
actors/<actor-UID>/ |
| written by | ateom | atelet |
| contents | one dead ateom.sock |
durable dir, checkpoints, bundle overlay |
| size | a few idle bytes | ~1.4 GB per actor |
| harm | dial+probe per stats sweep, cachedPools retention |
node disk pressure, atelet eviction |
| trigger | any pool rollout | worker pod deleted or de-scheduled with actors bound |
#1677's proposed janitor (cmd/atelet/ateomgc.go) is close in shape to the sweep described above — both modeled on imagegc.go, both node-scoped, fail-closed, with a min-age. If both land it is worth deciding whether they should be one loop.
Documentation gap. There is no written description of atelet's on-disk layout or how it does bookkeeping, which is part of why the ownership and lifetime of these directories is unclear.
Confirmation
- I have searched existing issues and verified that this is not a duplicate.
- I have verified that this issue occurs on the latest commit on
main.
- Dominant language
- Go
- Stars
- 2k
- Forks
- 333
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 275
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from agent-substrate/substrate
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
agent-substrate/substrate#1750 · 1 comment ·
-
area/dev-infra area/microVM kind/cleanup
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
agent-substrate/substrate#1695 · 2 comments ·
-
area/node area/observability good first issue kind/feature prio/P2
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
agent-substrate/substrate#1647 · 1 comment ·
-
area/cli kind/bug kind/docs
Difficulty 2/5 Half a day Newbie friendliness 78/100
agent-substrate/substrate#1642 ·
-
area/demos kind/bug
Difficulty 1/5 Under an hour Newbie friendliness 85/100
agent-substrate/substrate#1579 · 1 comment ·
All issues in agent-substrate/substrate
Similar issues
-
optimization optimization:agents-md-curator
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
githubnext/gh-aw-cao#13143 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
blinklabs-io/bursa#904 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
yanet-platform/ipfw-go#129 ·
-
bug confmap/provider/googlesecretmanagerprovider needs triage
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
open-telemetry/opentelemetry-collector-contrib#51273 · 2 comments ·
-
bug: AI Gateway client filter lists "Unknown" twice when NULL and literal Unknown clients coexist Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 90/100