Commit Graph
86 Commits
Author SHA1 Message Date
Dmitry Berkovich ca81df94ec Record the pause capture scope in LocalSnapshotInfo (#812)
Part of #791 (PR 2 of the [implementation
plan](https://github.com/agent-substrate/substrate/issues/791#issuecomment-5226674097)).

Adds `LocalSnapshotInfo.content_scope` and records the template's
`onPause` scope in `FinalizePausedStep`. A later suspend of a PAUSED
actor needs to know what the local pause snapshot actually captured, to
validate the requested scope conversion *before* the actor leaves PAUSED
— even if the template's `onPause` is edited while the actor sits
paused. The snapshot's on-node manifest (#810) remains the authoritative
record at upload time; this control-plane copy is for early validation
only.

- `ateapipb` proto: `SnapshotContentScope content_scope = 3` on
`LocalSnapshotInfo` + regen
- `FinalizePausedStep`: sets `ContentScope` from
`SnapshotsConfig.OnPause` when recording `LocalSnapshotInfo`
- Unset for actors paused before this field existed (consumers fall back
to the template's `onPause`)

Tests:
- New `TestFinalizePausedStep_RecordsContentScope` (OnPause
Data/Full/unset→Full, through a real store with a worker)
- `TestFinalizePausedStep_WorkerGone` now seeds the template the step
reads
- `TestPauseActor`: expected `ContentScope` added — which exposed a
latent test bug: the custom `snapshot_prefix` comparer was asymmetric
(`strings.HasPrefix(y, x)` only); go-cmp requires Comparers to be
symmetric and probes them with swapped arguments, and the new field
shifted the comparison order so the probe finally landed on it. Fixed by
matching the prefix in both directions.

Verified end-to-end on a GKE deployment: a paused actor's Valkey record
now carries `"contentScope": "SNAPSHOT_CONTENT_SCOPE_FULL"` in
`localSnapshotInfo`.

`go test -race ./cmd/ateapi/...`, `go vet`, `gofmt` clean. Independent
of #810; no dependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-08 09:46:25 -07:00
Haven Xia 3d33153239 Make envtest offline-capable and factor out TestMain boilerplate (#792)
`setup-envtest use` with no version re-resolves "latest" against the
remote release index on every run, so the envtest-backed packages needed
network even with fully cached binaries.

This PR pins the control plane to `1.36.x`, and fold the three
per-package `envtest` into `internal/testenv.Start`, which also honors
`-short`: `go test -short ./...` skips the envtest-backed packages and
is the guaranteed-offline path.

Have Claude to help me fix it

Fixes #789

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-08-07 12:07:31 -07:00
Elijah Rodriguez-Beltran 8152de17df Integrate with substrate
CONV=a6947013-0eab-49f2-9960-11d98ee43dbf
2026-08-07 18:20:22 +00:00
Eitan Yarmush c9777b49c4 atunnel: broker actor certificates through atelet (#708)
Closes #706

## Summary
- broker short-lived actor certificates from atelet over a same-node
mTLS Unix socket
- keep the actor private key in atunnel and renew the certificate before
expiry
- authenticate egress CONNECT using the actor certificate instead of
bearer tokens

## Testing
- `make verify`
- `go test -race ./internal/atunnel`
2026-08-07 09:10:34 -07:00
Krisztian F 0dbe1523e4 feat(otel): add cold start metrics (#776)
This PR implements the last two metrics from #433.

Right now, we can see that a resume was slow but not where.
`ate.actor.lifecycle.operation.duration` covers the whole ateapi
operation, and `atenet.router.route.duration` covers the edge, but
everything between ateapi-atelet-actors is one block that contains
fetching the manifest, downloading the snapshot, unpack the OCI image,
call to ateom.

We have `rpc.server.call.duration` that gives us the atelet restore
total time, but template, kind, and scope labels are missing, so today
we cannot really pinpoint why/where we have a regressions in latency.

In this PR I am adding per-phase histograms, here's an example of what
we can know after these changes:

```console
ateom_restore   522 ms   ###############################
download        8.9 ms   #
manifest_fetch  3.2 ms
oci_unpack      2.6 ms
---------------------------------------------------------
total           535 ms
```

It also fixes a gap #683 opened where a `data_on_golden` resume was
labeled identically to a plain one on the lifecycle histogram.

Things folks might want to argue with:
- total as a phase value. Partly duplicates `rpc.server.call.duration`,
but that one has no domain labels and gRPC-specifc. We can drop it, but
it's an inferior operational UX, so I'd rather have it here.
- Phases overlap, they are not a partition of total, because download
runs concurrently with the asset fetch and unpack. I called this out in
the metric description. Do not sum across phases.
- New `ate.snapshot.scope` key rather than a new `ate.snapshot.kind`
value for `data_on_golden`. A new value would collapse local and
external into one bucket, which is the biggest latency difference there
is. This does add a label to the already shipped lifecycle histogram.

Verified on kind, and all e2e suites pass, and the emitted series cover
every kind (golden, latest, local) on both metrics with no unknown
values.
 
- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-07 10:15:19 -04:00
Angela 058b104dec feat: Add control plane telemetry - scheduler eligible workers histogram (#682)
Part of #564 (Part 3)

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR

## Description
This PR implements Part 3 of #564 by adding telemetry histogram
instrumentation for `ate.scheduler.eligible_workers` in `ateapi`. It
measures unassigned free worker capacity remaining after all scheduling
constraint filters are applied, sampled at every scheduling decision.

## Key Changes:
* Updated `Scheduler.Schedule()` to record eligible candidate workers
per pool (`recordEligibleWorkers`).
Defined `SchedulingConstraintKey` (`ate.scheduling.constraint`) and
constraint classification values (`none`, `required_nodes`, `selector`)
in `internal/ateattr/ateattr.go`.
* Added unit tests in `scheduling_test.go` covering candidate counts,
namespaced attributes, zero-capacity fleet states, empty fleets, sandbox
class mismatches, draining workers, and constraint classifications.

## Testing
* `go test -buildvcs=false ./cmd/ateapi/internal/controlapi/...`
* `go test -buildvcs=false ./cmd/atenet/internal/router/...`
* `make test`
## E2E  Test
* `./hack/create-kind-cluster.sh`
* `./hack/install-ate-kind.sh --deploy-ate-system --deploy-demo-counter`
* `./hack/run-e2e.sh ./internal/e2e/suites/metrics/...`
2026-08-07 09:29:20 -04:00
Troy Chiu 97489938a8 api: add actor_uid to Assignment alongside actor (#736)
Fixes https://github.com/agent-substrate/substrate/issues/653 by adding
`actor_uid` to `Assignment` alongside `actor`

- [X] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-08-06 14:20:45 -07:00
Luiz Oliveira aa9b7b82cd Make UpdateActorSnapshot follow substrate API guidelines (#758)
Fixes #732  

* `UpdateActorSnapshot` now carries the resource itself + `update_mask`
* `scope` is now applied via the update mask
* Moved `update_mask` to a separate file, so it can be reused by other
update RPCs.
* Added `uid` and `version` as optional guards. 


- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-05 23:10:17 -04:00
Eitan Yarmush 8b2dd1bbc3 Fix/actor snapshot tag update cas (#755)
Removes unnecessary locking in `UpdateActorSnapshotTag`
2026-08-05 17:01:14 -04:00
Angela 1f8f842a64 feat: Add control plane telemetry - actor crashes counter and operation labels (#642)
Fixes #564 (Part 2)

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR

## Description
This PR implements the second part of #564 by adding telemetry counter
instrumentation for `ate.actor.crashes` in `ateapi` and establishing
bounded label validation for operations and crash failure causes.

## Key Changes:
- Updated `maybeCrashActor` and `crashActor` to reference
`ateattr.OperationName*` constants and normalize `opName`.
- Passed explicit failure reasons (`ReasonCorruptedAssignment`,
`ReasonWorkerPodGone`, `ReasonWorkerReassigned`) across
`workflow_resume.go`, `workflow_suspend.go`, and `workflow_pause.go`.
- Instrumented `WorkerPoolSyncer.releaseActorOnDeadWorker` in
`syncer.go` to record `recordActorCrash` when background pod deletion
crashes an actor.
- Instrumented `FinalizePausedStep` in `workflow_pause.go` to record
`recordActorCrash` when `nodeName` is missing during pause finalization.
- Added `ate_actor_crashes` to `PlatformMetricPrefixes` in
`collector_metrics.go`.
- Added pre-creation resource cleanup in `metrics_test.go` to prevent
`AlreadyExists` errors on test reruns.
- Updated `metrics_test.go` to trigger an actor crash via `UpdateActor`
and `ResumeActor`, asserting `ate_actor_crashes` emission and label
presence in OTel collector scrape outputs.

## Testing
* `go test -buildvcs=false ./cmd/ateapi/internal/controlapi/...`
* `go test -buildvcs=false ./cmd/atenet/internal/router/...`
* `make test`
## E2E  Test
* `./hack/create-kind-cluster.sh`
* `./hack/install-ate-kind.sh --deploy-ate-system --deploy-demo-counter`
* `./hack/run-e2e.sh ./internal/e2e/suites/metrics/...`
2026-08-05 16:31:43 -04:00
Luiz Oliveira b11c6e07ff Make UpdateActor method to follow API guidelines (#742)
#732  

* `UpdateActorRequest` now carries the resource itself + `update_mask`
* `worker_selector` is now applied via the update mask (making it
possible to clear the worker selector)
* `update_mask` is validated against an allowlist of paths. Currently,
only `worker_selector` is allowed.
* Added `uid` and `version` as optional guards. 

I'll update UpdateActorSnapshotTag in a follow-up PR


- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-05 16:28:59 -04:00
Troy Chiu de4c3dba24 ateapi/controlapi: process syncer events via rate-limiting workqueue (#674)
Refactors `WorkerPoolSyncer` to process events through a rate-limiting
workqueue instead of processing them synchronously inline.

Previously, store errors during Informer event handling, such as 
- optimistic locking version conflicts (`store.ErrVersionConflict`)
- create races (`store.ErrAlreadyExists`)
- `WorkerPool` not yet present in the lister cache

were logged and dropped, leaving store state out of sync until a
subsequent pod event occurred.

- [x] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-08-05 10:49:29 -04:00
Julian Gutierrez Oschmann 4f2fd4912f Wrap worker pod metadata into a WorkerAssignment message. (#737)
This change formalizes the invariant that all these fields are either
all set or not. This simplifies some client code that was just checking
random worker metadata fields to see if the actor has an assignment or
not.

It also fixes a bug in `{Pause,Suspend}Actor` where we were not cleaning
up the worker pod UID field.
2026-08-04 17:08:58 -07:00
Krisztian F 6f2b2178c1 feat(otel): add actor lifecycle + scheduler duration metrics (#514)
This PR is the second slice of the platform-metrics split (#433).

It adds two duration histograms emitted by ateapi:
- `ate.actor.lifecycle.operation.duration`:
create/resume/suspend/pause/delete, labeled by operation, template,
pool, sandbox class, and (on resume) snapshot kind
- I'd like to use this for user-facing latency for e.g. showing when
suspended actors can serve requests again. The existing
`rpc.server.call.duration` metric covers this, but the meaningful
dimensions are missing, so it's not really actionable. Extending its
labels with extra, domain-specific labels is an OTel anti-pattern, hence
the new metric.
- `ate.scheduler.assignment.duration`: worker-assignment step, labeled
by outcome (assigned / no_free_worker / error) and pool
- I'd like to use this to alert on no free worker situation, and have
proper SLOs via various percentiles for assigning latencies. There's no
RPC around this, so it's not something existing RPC metrics cover.

Tested e2e on a local kind cluste where: both metrics reach the
otel-system collector with the expected labels (resume shows
`snapshot_kind=golden`, scheduler shows `outcome=assigned`, no
`error.type` on success).

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-04 13:57:55 -04:00
Maya WangandMaya Wang 9e3ee7a3d8 feat: readyz: make the overall wait timeout configurable per template (#487)
> **Rescoped again.** This PR previously proposed
`--golden-snapshot-warmup`, a
> tunable wall-clock delay before the golden checkpoint. Per discussion,
that
> direction is dropped: the answer for a workload that cannot report
readiness
> is a readiness endpoint — a small sidecar where the workload itself
cannot be
> changed — not a longer timer. What survives is the piece that
discussion
> agreed on, and which the previous revision already flagged as a
follow-up:
> making the readyz deadline itself configurable.
>
> The warmup work is not in this branch. It is kept locally in case a
workload
> genuinely cannot be given a readiness signal before GA, and would come
back as
> its own PR if so.

## Summary

`readyz.Wait` polls until the container returns 200 or a **hardcoded
30s**
elapses. A workload that legitimately takes longer to bind its HTTP
server
cannot be accommodated without raising the ceiling for every actor in
the
cluster, and losing that race fails the actor start.

How long a workload takes to become ready is a property of that
workload, so
this makes the deadline a per-template setting rather than a package
constant.

Adds optional `timeoutSeconds` to `ContainerReadyz`. **Unset keeps
today's 30s**,
so no existing template changes behavior.

## Changes

The value rides on the existing probe, so it follows the chain the probe
already
takes and no call site needs to know about it:

`ContainerReadyz.timeoutSeconds` → `toAteletReadyz` → `ateletpb.Readyz`
→
`toAteomReadyz` → `ateompb.Readyz` → `readyz.Wait`

- `pkg/api/v1alpha1/actortemplate_types.go` — `TimeoutSeconds *int32`,
  `+optional`, `Minimum=1`, `Maximum=3600`.
- `internal/proto/ateletpb/atelet.proto`,
`internal/proto/ateompb/ateom.proto` —
  `int32 timeout_seconds = 2` on both `Readyz` messages.
- `cmd/ateapi/internal/controlapi/workload_spec.go`,
`cmd/atelet/main.go` — pass
  it through the two conversions.
- `internal/readyz/readyz.go` — `OverallTimeout` becomes
`DefaultOverallTimeout` (still 30s) and `Wait` resolves its deadline
through a
  new `overallTimeout(probe)` helper.
- Regenerated: both `.pb.go`, `zz_generated.deepcopy.go`, and the
  `actortemplates` CRD.

None of the four `readyz.WaitAll` call sites change.

**On the zero value.** Unlike a warmup delay — where zero is a real
request
meaning "checkpoint immediately" — a zero readiness deadline could never
be met,
so it is never something a template author means. A non-positive value
on the
wire is therefore read as "unset" and falls back to the default, and the
CRD
field is a pointer with `Minimum=1` so the API rejects `0` outright
rather than
silently substituting 30s behind the author's back.

**On bounding**, which was the open question left on the previous
revision:
bounded at `3600`. A template asking to wait longer than an hour for
readiness
is expressing a broken workload, not a slow one, and the bound keeps a
typo from
pinning a worker for a day.

## Verification

- `go build ./...`, `go vet ./...`, `gofmt`, `go test ./...` — all pass.
- `internal/readyz/readyz_test.go` — `overallTimeout` resolves unset and
negative to the default and honors an explicit value; `Wait` against a
port
nothing binds gives up at the probe's 1s deadline rather than the 30s
default.
- `workload_spec_test.go`, `cmd/atelet/main_test.go` — the timeout
crosses both
  conversions, and a probe without one stays zero on the wire.
- `actortemplate_validation_test.go` — the bounds are enforced by a real
API
server. This suite runs under envtest against the generated CRD
directory, so
it exercises the regenerated `actortemplates` CRD rather than the Go
markers:
  `300` is accepted, unset is accepted, and `0`, `-1` and `3601` are all
  rejected by apiserver schema validation.

- **On a real cluster, via CI.** `internal/e2e/fixtures/probe` now
declares a
`readyz` probe with `timeoutSeconds: 60`, pointed at the `/healthz` the
probe
binary already serves on `:80`. The kind e2e that runs on every PR
therefore
exercises the value crossing ateapi → atelet → ateom on real binaries,
across
the auth matrix, on both the run and restore paths. This is also the
readyz
  path's first e2e coverage — no fixture declared a probe before.

Wire compatibility degrades safely in both skew directions:
`timeout_seconds` is
a new field 2 on a `Readyz` message that has only ever had field 1, so
an old
ateom ignores it and an old ateapi leaves it zero, which reads as the
30s
default.

No GKE run. What that would add over the above is a workload whose
readiness
genuinely exceeds 30s, and that is the readiness-sidecar work rather
than this
PR.

Fixes #<issue_number_goes_here>

> It's a good idea to open an issue first for discussion.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR

---------

Co-authored-by: Maya Wang <mymaya@google.com>
2026-08-04 07:07:41 -07:00
Dmitry Berkovich 3ed6aa07e1 microvm: API support for resume Data snapshots on the golden snapshot (#451, phase 1) (#683)
Phase 1 of #451, micro-VM only. gVisor support will follow in a separate
PR.

## What

Adds a `snapshotsConfig.onResume` block to the ActorTemplate, selecting
per snapshot situation what supplies the guest state at resume: each
field names what is being resumed *from*, the value names the boot
source. `onPause`/`onCommit` remain pure capture scopes (`Full | Data`),
and the `ActorSnapshot` records plain `Full`/`Data` content; the
golden-combine is strictly a restore-time behavior.

```yaml
snapshotsConfig:
  onPause: Data
  onCommit: Data
  onResume:
    fromData: Golden   # ColdBoot (default) | Golden — Data snapshots resume as
  location: gs://…     # golden memory + fs delta with the actor's data layered on
```

Under `fromData: Golden`, a data-only restore combines the template's
**golden snapshot** (guest memory + full fs delta) with the actor's
captured durable data — the actor comes back with the golden's warm
state over its own data instead of cold-booting. `fromData` applies to
every resume of a Data snapshot — commit or pause checkpoint. Future
situations (e.g. resuming a Full snapshot across a template upgrade,
#477) get sibling fields under `onResume` rather than overloading this
one.

## How

- **CRD** (`pkg/api/v1alpha1`): new `OnResumeConfig`/`ResumeSource`
types; `fromData: Golden` is CEL-gated to `sandboxClass: microvm`.
- **Control plane** (`ateapi`): resume derives the behavior from the
template's `onResume` configuration — for a Data durable snapshot or a
Data pause checkpoint it resolves the golden snapshot's location and
sends the restore-only `DATA_ON_GOLDEN` wire scope, failing early (with
an actionable error) if the golden snapshot is missing or not Full.
Golden actors always commit `Full` regardless of `onCommit` — their
snapshot is the base the combine needs.
- **Wire APIs** (`ateletpb`/`ateompb`): restore-only
`SNAPSHOT_SCOPE_DATA_ON_GOLDEN`, rejected on checkpoints;
`golden_snapshot_uri_prefix` is a top-level `RestoreRequest` field
because the actor's snapshot may be a local pause checkpoint while the
golden snapshot is always external.
- **atelet**: stages a single combined folder — the actor's files (the
durable-dir tar) win name collisions, the golden snapshot supplies the
rest. External restores download both halves concurrently; local (pause)
restores copy the actor's files from the local checkpoint dir while the
golden's files download in parallel. The golden manifest's pinned
sandbox binaries run the restored guest and are recorded on-node for
later checkpoints.
- **ateom-microvm**: `DATA_ON_GOLDEN` restores through the existing Full
path — cloud-hypervisor relaunches from the golden's guest files while
the durable virtio-fs share serves the actor's re-materialized data.
gVisor rejects the scope (defense-in-depth behind the CRD gate).

## Testing

- Unit: envtest CEL cases, converter tables, atelet request validation
(checkpoint rejection, golden-URI rules incl. local+golden),
combined-download test against a fake object store, resume-workflow
golden-resolution cases for both the durable and pause paths.
- e2e (`suites/demo`): OnGolden lifecycle cases for the commit path, the
pause path, and a two-durable-volume variant (micro-VM lane only),
asserting counters across pause/suspend and the recorded Data content
scope.
- Manually verified on a live GKE cluster: Data commit and Data pause
both resume with the golden guest's memory over the actor's data, on
different pods.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-01 20:13:37 -07:00
Angela e70e1f21f8 feat: extend edge routing telemetry with resume and outcome labels (#591)
Fixes #564 (Part 1)

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR

## Description
This PR implements the first part of #564 by extending metric
`atenet.router.route.duration` with singleflight activation state
(`ate.router.resume`) and outcome status labels (`ate.router.outcome`).
All telemetry attribute keys and label value sets are centralized in
`internal/ateattr` to ensure consistency across components.

## Key Changes:
* Added `resumed` boolean to `ResumeActorResponse` in `ateapi.proto`
indicating whether a cold activation workflow occurred
(`!state.WasRunning`).
* Updated `workflow_resume_test.go` to assert `resumed == true` on cold
activations and `resumed == false` for already-running actors.
* Extended `classifyOutcome(err)` in Envoy `ext_proc` to classify errors
into metric labels: `ok`, `cancelled`, `timeout`, `no_capacity`,
`lock_conflict`, `not_found`, `unavailable`, `rate_limited`, and
`resume_error`.
* Added explicit handling for `codes.Unavailable` (`"unavailable"`) and
`codes.ResourceExhausted` / `StatusCode_TooManyRequests`
(`"rate_limited"`).
* Consolidated metric label names on `atenet.router.route.duration`
using standard keys from `internal/ateattr`:
     * `ate.template.namespace` (`TemplateNamespaceKey`)
     * `ate.template.name` (`TemplateNameKey`)
     * `ate.router.outcome` (`RouterOutcomeKey`)
     * `ate.router.resume` (`RouterResumeKey`)
* Updated `manifests/ate-install/kind/kustomization.yaml` to export
`atenet-router` OTLP metrics to `opentelemetry-collector`.

## Testing
* `go test -buildvcs=false ./cmd/ateapi/internal/controlapi/...`
* `go test -buildvcs=false ./cmd/atenet/internal/router/...`
* `make test`
## E2E  Test
* `./hack/create-kind-cluster.sh`
* `./hack/install-ate-kind.sh --deploy-ate-system --deploy-demo-counter`
* `./hack/run-e2e.sh ./internal/e2e/suites/metrics/...`
2026-07-31 13:41:56 -04:00
Eitan Yarmush ef7b29da44 ateapi: add ActorSnapshot lifecycle APIs 2026-07-30 19:40:59 -07:00
Eitan Yarmush 38ade556ee ateapi: store self-describing snapshots under stable paths 2026-07-30 11:50:59 -07:00
Michelle Au ac3f08f7ad Handle partial volume creation failures (#527)
Part of #232

1. During CreateActor, the volumes to be created get instantiated in the
Actor API in PENDING status. The Actor is still instantiated as
SUSPENDED.

However, the CreateVolume operation is moved to the beginning of
ResumeActor. This is because CreateActor is intentionally designed to
not be idempotent. The downside is that the user won't know dependent
resource creation failed until the first resume.

2. Changes volume id to use actor uid instead of atespace+actorname and
adds "substrate" prefix

3. Adds a new DELETING status that gets persisted before we start to
delete volumes.

This shifts the existing DB DELETE precondition checks to the new
DELETING status. And now you can only delete the actor in DB from
DELETING status.

4. Refactor DeleteActor to use a workflow

In the future, we will also add a workflow that will do mandatory node
cleanup of volumes before starting to delete the volumes.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-30 14:21:45 -04:00
Mesut Oezdil 8572d86709 ateapi: key the workerpool metric by namespace (#632)
`ate.workerpool.workers` tallies by pool name, state, and sandbox class,
but a WorkerPool is namespaced, so two pools that share a name sum into
a single series. This is reachable today: the counter demo and the
autoscaled-workerpool demo both create a pool named `counter`, and with
both installed the demo HPA scales on the other pool's assigned workers.
Adds `ate.workerpool.namespace` to the tally key and attributes, and
pins it in the demo's adapter query and HPA selector.
2026-07-30 13:09:35 -04:00
Mesut Oezdil 22864b882f ateapi: check pod deletion before worker eligibility in syncer (#638)
`syncWorkerToStore` checks `pod.Status.PodIP != ""` before
`DeletionTimestamp`, so a Terminating pod reporting no IP never reaches
`markWorkerDraining` and stays `STATE_ACTIVE`, schedulable for as long
as the pod lingers. Draining resolves the stored record by name and
never reads the IP, so this swaps the two guards.

The startup reconcile runs through the same function, so a pod already
Terminating when ate-api-server restarts is now marked too.

`markWorkerDraining` already runs on every update event for a
Terminating pod, so this adds no new call pattern.

Fixes #600
2026-07-30 13:02:01 -04:00
Haven Xia e3fe79d45c Clear AteomPodUid when releasing an actor from a dead worker (#633)
Tiny one.

Fixes #618

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-30 12:36:55 -04:00
sfunkenhauser 87698ea9c6 Handle graceful worker termination (#517)
Part of [#23](https://github.com/agent-substrate/substrate/issues/23)

The overall plan is:
1) Trap SIGTERM in the ateom, and forward SIGTERM to all application
containers.
2) Once all application containers exit, ateom will exit itself.
3) The workerpool syncer will mark workers as 'DRAINING' when a pod
deletion is triggered (pod.DeletionTimestamp) to prevent actors from
being scheduled. On the pod Deleted event, the worker and actor (if it
was assigned) will be cleaned up.

This PR implements 3), there are 2 more PRs coming which will implement
1) and 2) for each ateom (gvisor and uVM).
2026-07-29 14:04:45 -07:00
Troy Chiu 5bf19fcae8 [minor] stamp OpenTelemetry span attributes in UpdateActor (#512)
This PR adds missing OpenTelemetry span attributes in UpdateActor and
corresponding tests.

- [X] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-07-29 15:06:31 -04:00
Julian Gutierrez Oschmann adbb24715f Add unit tests for request validators in ate-apiserver. (#590)
Add unit tests that exercise all the validation cases and assert on the
k8s `ErrorMatcher` instead of comparing raw error strings.

Moved most validations from functional tests to unit tests, and kept one
validation for each gRPC method to validate that handlers properly map
Go errors to gRPC statuses.
2026-07-29 13:45:37 -04:00
Julian Gutierrez Oschmann f83b65d150 Update RPC error messages to include both actor name and atespace.
Now that we pass actorRef around, make sure we use it to report
the full name (atespace + name) of the actor when reporting errors.
2026-07-28 12:33:23 -07:00
Julian Gutierrez Oschmann 2dc1fc6266 Introduce an ActorRef type to bundle actor atespace and name.
The pattern of passing a tuple of atespace and actor name together
is across the whole codebase. This simplifies both callers and callees
by leting them pass a single field that bundles both.

The ActorRef is the actor specific, typed,
in-process version of the ObjectRef we have in our gRPC API.
2026-07-28 12:33:23 -07:00
Haven Xia 60d484b77d rename ErrPersistenceRetry to ErrVersionConflict to reflect the actual behavior (#560)
The error is only returned on an optimistic-concurrency version
mismatch. Rename it for that behavior so other retryable errors don't
get folded into it (but should introduce their own error name).

Followup for post-merge comment in #535 

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-28 10:00:32 -04:00
Haven Xia bb9d9a1b03 Refresh the actor when AssignWorkerStep's write hits a version conflict. (#535)
Refresh the actor when `AssignWorkerStep`'s write hits a version
conflict so that the workflow won't kept retry with a stale version
actor and never succeed.

AI assisted on testing. 

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-27 17:23:14 -07:00
Julian Gutierrez Oschmann 3e6da2eb29 Extract scheduling logic into its own package. (#546)
The scheduling logic is all mixed up with the resume/suspend/pause
workflows. Move it into a dedicated package, behind an interface and add
tests.
2026-07-27 11:28:52 -07:00
Zoe Zhao 9890219151 Added mTLS between ate system components (#237)
Part of [#170](https://github.com/agent-substrate/substrate/issues/170)
Establishes mutual TLS between all ate system components, and updates
the certificate plumbing it depends on.

Main changes:
1. The atenet router now verifies ate apiserver' serving certificate,
and presents its client cert to ate apiserver. Previously the connection
used `InsecureSkipVerify`.
2. AteApi server verifies atelet's serving cert.

Minor bug fixes:
1. Prevent `servicednssigner` from signing a cert with no DNS SANs. Also
updated valkey cluster's cert configuration, because it was relying on
the cert with empty DNS.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-24 15:43:03 -07:00
Michelle Au 9300387fef Add basic per-actor external volume flow (#405)
Initial part of #232.

Extends the ActorTemplate API to add support for per-actor external
volumes and adds control plane and atelet hooks following the Actor
lifecycle.

Callouts to volume operations are abstracted with a Volume interface.
Right now, only a mock volume plugin for testing has been implemented,
but we will add CSI support next.

- [x] Tests pass
- [ ] Appropriate changes to documentation are included in the PR - Not
going to update documentation until we add CSI support.
2026-07-24 14:40:26 -07:00
Mesut Oezdil aeb6ec5a69 fix: use background context for async worker release (#498)
AssignWorkerStep starts a background goroutine to release a stale
worker assignment. It builds its context from the request ctx, but
that ctx is cancelled right after the workflow returns (releaseLock
runs on defer). If the goroutine has not called UpdateWorker yet, the
call fails with a cancelled context and the worker keeps the stale
assignment forever. Nothing else clears it while the pod is alive.

Fix: use context.Background() for the release call, same pattern used
in cmd/atenet/internal/router/resumer.go.

Tested with go test ./cmd/ateapi/internal/controlapi/...
2026-07-23 19:14:51 -07:00
Haven Xia c7aa4a2559 Same crashing for workflow_pause.go when pod is gone (#508)
Should found it when writing #474..
And also correct an error in `workflow_resume.go`.


- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-23 17:37:34 -07:00
Julian Gutierrez Oschmann f38f94b519 Rework the Lock API so that locks can be auto-renewed. (#482)
Instead of requiring callers to specify a fixed deadline for the lock,
let the store return a `Lock` object that performs auto-renewal in
background.

This fixes an issue when a workflow takes longer than the hard-coded
TTL, most commonly when atelet takes long times to initialize an actor
(e.g. pulling / untaring large OCI images).

Leases are taken for 30 seconds, but renewed every 10 seconds (TTL/3).

Removing that implicit TTL-based limit means a caller that sets no
deadline of its own (e.g. kubectl-ate) could otherwise hang a
Resume/Suspend/Pause indefinitely. MaxDeadlineUnaryInterceptor adds a
10-minute ceiling on every RPC as a hang-prevention backstop.
2026-07-23 14:13:30 -07:00
Krisztian F 163bcf4aa8 feat(otel): add ate.workerpool.workers metric with e2e suite (#492)
Part of the preferred split of #433, please check that out if you need
wider context.

This PR adds `ate.workerpool.workers` metric end to end so we have
coverage for workers by pool / state / sandbox class, emitted by ateapi.
Comes with the few attribute keys it actually uses, unit tests, and an
e2e suite that drives an actor lifecycle and checks the metric (plus the
two existing platform metrics) actually lands in the collector.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-23 14:07:09 -04:00
Zoe Zhao 5cb0a26443 Release the worker and clear the actor's binding when crashing an actor. (#475)
This will free up the worker to which the crashed actor was assigned, so
it can be picked up by other actors.

Minor fix: 
* Added functional test to make sure a crashed actor can be deleted.
2026-07-21 12:28:32 -07:00
Zoe Zhao d49b0fc9cf Fix retry AssignWorkerStep: only save the assigned worker after having picked and persisted worker assignment (#478)
Fixed flakey integration test at head.

Broken test in
https://github.com/agent-substrate/substrate/actions/runs/29795626928/job/88526261507
was because, once a old version of worker has been picked and saved in
state.worker, retrying no longer picked a new, usable worker.

Updated the implementation to only update state.worker after having
successfully picked and persisted the worker.
2026-07-20 22:10:56 -07:00
Zoe Zhao 4ecf3fbf6c Add CheckPrerequisite to WorkflowStep to validate state machine edges (#374)
Fixes #369

Add a CheckPrerequisite method to the WorkflowStep interface, called by
RunWorkflow after IsComplete returns false and before Execute, so that
each workflow validates its actor state-machine edge up front while
retried (reentrant) workflows still fast-forward past completed steps.
2026-07-20 19:21:34 -07:00
Haven Xia a2c2e1ca49 Clear a never-written snapshot when suspending a dangling worker (#474)
When the worker pod disappeared before Checkpoint was called, the
suspend workflow skipped the atelet call but still let finalize promote
the reserved InProgressSnapshot URI to LatestSnapshotInfo, overwriting
the last good snapshot with one that was never written. This could break
the next resume call of this actor.

Now the dangling-worker path crashes the actor via `crashActor` too, and
crashActor clears any reserved snapshot URI so `FinalizeSuspendedStep`
won't overwrite the last good snapshot.


- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-20 17:05:20 -07:00
Julian Gutierrez Oschmann 509f4c2170 Update ActorTemplate's container spec to mirror k8s semantic for command and args (#462)
* Add `args` field in addition to `command`.
* Implement logic in atelet to resolve both argv from the actor template
fields and the OCI image config (`entrypoint` and `cmd` fields).
* Update demos to not hardcode command as now we honor the image
default, which makes it not necessary.

Fixes #456
2026-07-20 16:15:05 -07:00
Krisztian F 3377b3fb59 feat(otel): set ate.* actor telemetry identity on ateapi spans (#412)
This PR sets consistent, namespaced actor-identity set on the spans we
already emit:

- `ate.atespace`, `ate.actor.id`, `ate.actor.template.{name,namespace}`,
`ate.actor.version`
- ateapi: the RPC server span for create/resume/suspend/pause/delete

We do this, so platform traces are queryable by actor, atespace, and
template. Cross-hop correlation and per-tenant/per-template filtering,
all on traces (not TSDB labels, as discussed in #174). This is a
general, workload-neutral telemetry identity plumbing. We could add
gen_ai specific stuff on top of this later.

This is a resume:
<img width="1913" height="1010" alt="image"
src="https://github.com/user-attachments/assets/876cf689-a8ec-4f37-b035-3b9b1de87d59"
/>

**Notes:**
- Left as it was on purpose: metric attribute names and the stdout log
labels (`ate.dev/*`), but we should probably do this as well. Maybe in
this PR?
- Bikeshed is welcomed on the `ate.*` spelling before merge, renaming
span attrs later is painful.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-20 09:00:15 -07:00
Julian Gutierrez Oschmann a2d55e99e0 Update Actor ID -> Actor Name references (#455)
Update all old references to actor id to refer to actor name.

Also fix benchmarking framework to use the latest version of the API.
2026-07-17 16:49:19 -07:00
Haven Xia 8a366bd653 Rework List endpoints to align with API style guide. (#423)
Rework List endpoints to align with API style guide.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-17 19:33:51 -04:00
Eric Bishop 381b3ad52c fix: make atelet / ateom use actor UID for its internal bookkeeping (#438) 2026-07-17 12:32:23 -07:00
Mesut Oezdil fc1fe4d91c fix: skip empty node name in local snapshot info after pause finalization (#329)
When the worker pod is gone from the DB during pause finalization,
nodeName stays empty.
The code still wrote NodeVmsWithLocalSnapshots: []string{""}, so on the
next ResumeActor the scheduler looked for a worker with node name "",
found none, and returned "no free workers available" permanently.
Fix: only set NodeVmsWithLocalSnapshots when nodeName is non-empty.
2026-07-17 10:24:04 -07:00
Troy Chiu a320da4a48 Remove DebugClear method from ate-apiserver (#446)
Fixes #443

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-17 12:45:01 -04:00
Julian Gutierrez Oschmann c1ab0958f8 Update atenet component to use actor *name* instead of *id*. 2026-07-10 13:06:09 -07:00
Haven Xia 669965a735 Rename actor_id to actor_name in atelet/ateom protos.
Aligns the internal ateletpb/ateompb services with the API style guide's
actor naming (field tags unchanged, so the wire format is compatible).

internal APIs keep dedicated identity fields (not the public ObjectRef).
2026-07-10 12:54:16 -04:00