Skip to content

Resuming a suspended actor detaches it when its node has no room - #2881

Merged
Zoe Zhao (zoez7) merged 1 commit into
agent-substrate:mainfrom
igooch:resume-elsewhere-detach
Oct 20, 2026
Merged

Zoe Zhao (zoez7) merged 1 commit into
agent-substrate:mainfrom
igooch:resume-elsewhere-detach

Conversation

@igooch

@igooch igooch commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

A suspended actor with a local snapshot is pinned to its node. While that node was full, its resume failed with ResourceExhausted even when other nodes had room, until atelet detached the actor on its own after --snapshot-detach-delay. Resume now detaches the actor itself, with DetachActor from #2852, and restores it on another node.

Part of #589. This covers a full node. A node that is gone, the scale-down case, is still open.

How the move works

Assignment still runs pinned to the node first. When it finds no room there, and a worker on another node has room, resume:

  1. Calls DetachActor on the pinned node. atelet uploads the latest snapshot and deletes the local copies.
  2. Records the detach, which marks the upload COMPLETED and clears the locality and assigned_node.
  3. Assigns a worker on any node and restores from the durable snapshot.

DetachActor is synchronous. If the snapshot isn't durable yet, the resume waits for atelet to upload it.

What the restore keeps

On a worker of the same runtime, the restore keeps the snapshot's fidelity. On a worker of another runtime, a MEMORY snapshot restores at ROOTFS, as for any durable-only actor since #2880. Process memory is lost, but rootfs writes and volumes are kept. ROOTFS and VOLUMES snapshots keep their fidelity on any worker of the same sandbox class. A template can't yet refuse the downgrade; #2818 would add that.

When the actor doesn't move

  • No worker anywhere has room. The resume returns ResourceExhausted as before, without calling atelet.
  • The detach fails. The resume fails, and the actor stays SUSPENDED on its node, not crashed, since it can still resume there.
  • The node is gone. DetachActor can't reach atelet, so the resume returns Unavailable and the actor stays pinned.
sequenceDiagram
    participant C as client
    participant A as ate-api-server
    participant L1 as atelet on node1 (full)
    participant O as object storage
    participant L2 as atelet on node2
    C->>A: ResumeActor
    A->>A: assign on node1: no room
    A->>A: node2 has room
    A->>L1: DetachActor
    L1->>O: upload latest snapshot
    L1->>L1: delete local copies
    L1-->>A: state with latest_durable only
    A->>A: mark COMPLETED, clear locality and assigned_node
    A->>A: reload restore source, assign on node2
    A->>L2: Restore(durable snapshot)
    L2->>O: download
    L2-->>A: ok
    A-->>C: RUNNING on node2
Loading

Design choices

  • Any runtime counts as room.
    • Memory is an optimization for GA, so a worker that can't restore the snapshot's process memory still triggers the move, and the actor restores there at ROOTFS.
    • Assignment still prefers a worker that keeps it.
  • No claim before the detach.
    • Claiming a worker first would leave the claim and the snapshot record disagreeing if ate-api-server crashed before the detach.
    • In this order, every step ends in a state resume already handles.
    • Two actors detached for one free place race for it, and the loser has paid an upload, but it ends durable-only and its next resume schedules it onto any node without another detach.

Also worth knowing

  • A concurrent update fails the record write with Aborted, and the next resume detaches again; atelet returns the same state. If the node has room by then, the resume restores there from the upload instead, and the record keeps the upload PENDING, because the detach cancelled atelet's report.
  • DetachActor errors map as the tag workflow maps PromoteSnapshot errors, plus atelet's Aborted passed through.
  • A resume that detaches records two scheduler samples, no_capacity then assigned. A dashboard that reads no_capacity as a failed resume will count these.
  • No API changes.
  • TestResumeActor_RelocatesAfterDetach checked ResourceExhausted at a point where node2 already had room, which now detaches. The check moved to before node2 exists.
  • The functional fake's DetachActor returns the named actor's last checkpointed snapshot, not the last of any actor.

@igooch
igooch force-pushed the resume-elsewhere-detach branch 3 times, most recently from 511eeec to 15b56ad Compare October 19, 2026 17:53
@igooch
igooch marked this pull request as ready for review October 19, 2026 17:54
// The runtime is not relaxed here: moving the guest state is worth an
// upload, giving it up is not.
constraints.RequiredNodes = nil
if hasRoom, err := w.anyWorkerHasRoom(ctx, constraints); err != nil || !hasRoom {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this room check move out of detachFromFullNode and into ResumeActor? Something like:

if shouldDetachFromNode(actor, err) {
	// Claim a worker on another node first.
	assigned, worker, err = w.assignWorkerOnOtherNode(...)
	if err != nil {
		return nil, false, err // still ResourceExhausted when no node has room
	}
	// Then detach from the full node.
	if err := w.detachFromFullNode(...); err != nil {
		// release the claim
		return nil, false, err
	}
}

Claiming the place before the detach closes the race noted at line 66, where two actors detached for the same place leave the loser durable-only. It also leaves detachFromFullNode with only the detach to do.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Before I make this change, a question for you and Michelle Au (@msau42).

DetachActor is synchronous. If the snapshot isn't durable yet, atelet uploads it before returning, and the resume waits for the whole upload. The upload runs on the RPC's context, so if the router's resume budget cancels the request, the upload is canceled too, and the next resume starts it over. With a claim taken first, every such attempt would also hold and release a worker for the length of the upload.

What if resume moves an actor only when its latest snapshot is already COMPLETED? Then DetachActor only prunes, which is fast. Claiming the worker first then costs almost nothing, and there's no upload to cancel. An actor suspended within the last --snapshot-upload-delay (5m by default) would get ResourceExhausted until atelet's background upload lands and is reported, and would move on the next resume after that.

The cost is that a recently suspended actor can't move right away. Is that acceptable for GA? If not, I'll claim first and keep the synchronous upload, and note the latency in the description.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I don't think we should wait for async upload at least with our current async period of 5 mins. We could force promote and resume faster than that.

  2. Claiming before DetachActor vs. keeping the current order (anyWorkerHasRoom -> DetachActor -> ensureWorkerAssigned): I'd recommend keeping the current order (checking room first, detaching while still SUSPENDED, then claiming in ensureWorkerAssigned) rather than claiming a worker before DetachActor:

  • If ateapi crashes between assignOnOtherNode and detach, then there will need to be some logic to handle the discrepency between assigned_node and snapshots.
  • Keeping the actor SUSPENDED and unclaimed until DetachActor + recordDetach complete preserves clean re-entrancy at every boundary. Even if two actors race for the last free worker and both detach, the loser ends in the normal durable-only SUSPENDED state (Locality = "", AssignedNode = "", Status = COMPLETED), which is still forward progress and lets the next retry schedule it onto any node without calling DetachActor again.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Thanks, that settles both. I'll keep the synchronous upload and the current order: room check, detach while SUSPENDED, then claim in ensureWorkerAssigned. That's what the branch does now, so no code change.

One follow-up I'd file rather than fix here: the upload runs on the RPC's context, so a resume the router cancels also cancels its upload, and the next attempt starts over. For a snapshot that takes longer than the router's budget to upload, only atelet's background upload would ever finish it. atelet could keep a foreground detach's upload going after the caller leaves.

@msau42 Michelle Au (msau42) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will also let Zoe review

// record this leaves behind. An UpdateActor takes no lease, so a conflict
// is retried on a fresh read.
stored, err := w.recordDetach(ctx, actorRef, actor, node, state)
for attempt := 1; isUpdateConflict(err) && attempt < recordDetachAttempts; attempt++ {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 should-fix 🟡 – The other workflows don't retry a version conflict in place. They return Aborted and let the caller re-enter the workflow, which picks up from the stored record. This step can do the same: write the record once and return the error.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Done. Zoe Zhao (@zoez7) Michelle Au (@msau42), this reverses the bounded retry msau42 asked for, so flagging it for both of you.

The step now writes the record once. A conflict returns Aborted, and the next resume re-enters from the stored record, detaches again, and records the state atelet returns. A test runs that sequence. The re-read before the detach stays, for msau42's stale-actor point.

The case the retry covered: if the actor's node has room by the time the resume re-enters, the resume restores there from the upload instead of detaching again. The record then keeps the upload PENDING, since the detach cancelled atelet's report. The description says so. The window is a concurrent UpdateActor landing during the detach, which takes no lease, so it should be rare.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A possible follow-up that closes the remaining gap is have a foreground DetachActor mark atelet's report due instead of cancelling it, so a record write that loses a conflict is corrected by the report atelet already retries.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning ABORTED is fine to be consistent with the other workflows. The important part was the re-read.

@igooch
igooch force-pushed the resume-elsewhere-detach branch from 5c269e9 to d856c35 Compare October 19, 2026 22:48
@msau42
Michelle Au (msau42) added this pull request to the merge queue Oct 19, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 19, 2026
A suspended actor whose latest snapshot is local to a node was pinned
to that node, so its resume failed with ResourceExhausted while the
node was full, even when other nodes had room. Until atelet detached
the actor on its own after the detach delay, nothing lifted the pin.

Resume still assigns a worker pinned to the node first. When that
reports no room, and a worker on another node has room, it calls
DetachActor on the actor's node, which makes the latest snapshot
durable and deletes the local copies. It records the detach, which
marks the upload COMPLETED and clears the locality and the assigned
node. The resume then assigns a worker on any node and restores the
actor from the durable snapshot.

Trying the pinned assignment first keeps the common path unchanged and
leaves a claim from an interrupted attempt to assignment's own
handling. A worker of another runtime counts as room: the actor
restores there at ROOTFS, without its process memory, as a
durable-only actor already does, and assignment still prefers a
worker that keeps it. The free place is not claimed before the detach,
so two actors detached for it race in their assignments, and the loser
is left durable-only.

The step re-reads the actor before it starts, and records the detach
of every copy on the node up to the last assigned generation, so a
state atelet returns with no durable snapshot still applies. A
concurrent update fails the record write with Aborted; the next
resume detaches again, and atelet returns the same state. A failed
detach fails the resume and leaves the actor on its node instead of
crashing it, since the actor can still resume there.
@igooch
igooch force-pushed the resume-elsewhere-detach branch from d856c35 to 15998a3 Compare October 19, 2026 23:49
@igooch

igooch commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Re-requesting review: the merge queue failed because #2879 landed after my last rebase and now rejects workers without a default runtime. I rebased and gave the test workers a runtime. Test fixtures only, no change to the code you approved.

@zoez7
Zoe Zhao (zoez7) added this pull request to the merge queue Oct 20, 2026
Merged via the queue into agent-substrate:main with commit 49ebabe Oct 20, 2026
15 checks passed
M.Besher Massri (besher-massri) added a commit to besher-massri/substrate that referenced this pull request Oct 20, 2026
…s lookup

Following agent-substrate#2883 and agent-substrate#2881, every suspended actor with a retained local snapshot
has AssignedNode set and passes RequiredNodes = [assignedNode] into
scheduling.Schedule on ResumeActor. Without a node index, Schedule calls
s.source.Workers(), copying the entire cluster worker slice and scanning every
worker in O(W) time on each node-pinned resume.

Index cached workers by node name in workercache.Cache and use O(1) node lookup
in scheduling.Schedule when RequiredNodes is set:
- Add byNode index and WorkersByNode(node) in
  cmd/ateapi/internal/workercache/workercache.go, maintained across relist,
  Create/Update/Delete watch events, and Forget.
- Add workerNodeLookup in cmd/ateapi/internal/scheduling/scheduling.go and query
  nodeLookup.WorkersByNode(node) when Constraints.RequiredNodes is non-empty,
  avoiding full-fleet slice allocation and linear scanning.
- Add TestCache_WorkersByNode, TestScheduleRequiredNodesUsesNodeLookup, and
  BenchmarkScheduleRequiredNodes.

Benchmark results (go test -run='^$' -bench='BenchmarkScheduleRequiredNodes' -benchmem -count=5 ./cmd/ateapi/internal/scheduling/...):
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1727404     600.6 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1822207     623.1 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    2066503     626.2 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1887322     684.0 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    2061903     665.5 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     83085   15061 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     82180   17377 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     72000   14448 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     71266   14944 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     86091   14398 ns/op      4160 B/op   2 allocs/op
M.Besher Massri (besher-massri) added a commit to besher-massri/substrate that referenced this pull request Oct 20, 2026
…s lookup

Following agent-substrate#2883 and agent-substrate#2881, every suspended actor with a retained local snapshot
has AssignedNode set and passes RequiredNodes = [assignedNode] into
scheduling.Schedule on ResumeActor. Without a node index, Schedule calls
s.source.Workers(), copying the entire cluster worker slice and scanning every
worker in O(W) time on each node-pinned resume.

Index cached workers by node name in workercache.Cache and use O(1) node lookup
in scheduling.Schedule when RequiredNodes is set:
- Add byNode index and WorkersByNode(node) in
  cmd/ateapi/internal/workercache/workercache.go, maintained across relist,
  Create/Update/Delete watch events, and Forget.
- Add workerNodeLookup in cmd/ateapi/internal/scheduling/scheduling.go and query
  nodeLookup.WorkersByNode(node) when Constraints.RequiredNodes is non-empty,
  avoiding full-fleet slice allocation and linear scanning.
- Add TestCache_WorkersByNode, TestScheduleRequiredNodesUsesNodeLookup, and
  BenchmarkScheduleRequiredNodes.

Benchmark results (go test -run='^$' -bench='BenchmarkScheduleRequiredNodes' -benchmem -count=5 ./cmd/ateapi/internal/scheduling/...):
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1802881     605.2 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    2008159     770.8 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1000000    1217 ns/op        96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1927760     839.7 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1705620     588.8 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     79581   17176 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     85873   14841 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     80368   15142 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     77570   14585 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     94237   17063 ns/op      4160 B/op   2 allocs/op
M.Besher Massri (besher-massri) added a commit to besher-massri/substrate that referenced this pull request Oct 20, 2026
…s lookup

Following agent-substrate#2883 and agent-substrate#2881, every suspended actor with a retained local snapshot
has AssignedNode set and passes RequiredNodes = [assignedNode] into
scheduling.Schedule on ResumeActor. Without a node index, Schedule calls
s.source.Workers(), copying the entire cluster worker slice and scanning every
worker in O(W) time on each node-pinned resume.

Index cached workers by node name in workercache.Cache and use O(1) node lookup
in scheduling.Schedule when RequiredNodes is set:
- Add byNode index and WorkersByNode(node) in
  cmd/ateapi/internal/workercache/workercache.go, maintained across relist,
  Create/Update/Delete watch events, and Forget.
- Add workerNodeLookup in cmd/ateapi/internal/scheduling/scheduling.go and query
  nodeLookup.WorkersByNode(node) when Constraints.RequiredNodes is non-empty,
  avoiding full-fleet slice allocation and linear scanning.
- Add TestCache_WorkersByNode, TestScheduleRequiredNodesUsesNodeLookup, and
  BenchmarkScheduleRequiredNodes.

Benchmark results (go test -run='^$' -bench='BenchmarkScheduleRequiredNodes' -benchmem -count=5 ./cmd/ateapi/internal/scheduling/...):
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1802881     605.2 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    2008159     770.8 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1000000    1217 ns/op        96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1927760     839.7 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1705620     588.8 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     79581   17176 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     85873   14841 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     80368   15142 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     77570   14585 ns/op      4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     94237   17063 ns/op      4160 B/op   2 allocs/op
M.Besher Massri (besher-massri) added a commit to besher-massri/substrate that referenced this pull request Oct 20, 2026
…s lookup

Following agent-substrate#2883 and agent-substrate#2881, every suspended actor with a retained local snapshot
has AssignedNode set and passes RequiredNodes = [assignedNode] into
scheduling.Schedule on ResumeActor. Without a node index, Schedule calls
s.source.Workers(), copying the entire cluster worker slice and scanning every
worker in O(W) time on each node-pinned resume.

Index cached workers by node name in workercache.Cache and use O(1) node lookup
in scheduling.Schedule when RequiredNodes is set:
- Add byNode index and WorkersByNode(node string) ([]*ateapipb.Worker, error) in
  cmd/ateapi/internal/workercache/workercache.go, maintained across relist,
  Create/Update/Delete watch events, and Forget, and returning an error when the
  cache is not ready.
- Add workerNodeLookup and workersForSchedule in
  cmd/ateapi/internal/scheduling/scheduling.go to query
  nodeLookup.WorkersByNode(node) when Constraints.RequiredNodes is non-empty,
  avoiding full-fleet slice allocation and linear scanning.
- Add TestCache_WorkersByNode, TestScheduleRequiredNodesUsesNodeLookup, and
  BenchmarkScheduleRequiredNodes.

Benchmark results (go test -run='^$' -bench='BenchmarkScheduleRequiredNodes' -benchmem -count=5 ./cmd/ateapi/internal/scheduling/...):
BenchmarkScheduleRequiredNodes/NodeIndex=true-8     849672    1203 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1000000    1157 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1086208    1237 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1000000    1248 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=true-8    1000124    1165 ns/op      96 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     35792   31085 ns/op    4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     37682   30128 ns/op    4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     39175   30108 ns/op    4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     36140   32745 ns/op    4160 B/op   2 allocs/op
BenchmarkScheduleRequiredNodes/NodeIndex=false-8     25226   80084 ns/op    4160 B/op   2 allocs/op
igooch added a commit to igooch/substrate that referenced this pull request Oct 21, 2026
… request times out (agent-substrate#2940)

Fixes agent-substrate#2910.

`DetachActor` ran its detach on the RPC's context. When a resume moved
an actor off a full node (agent-substrate#2881) and the caller left mid-upload, for
example when the router's resume budget ran out, the upload was
cancelled and the next resume started it over. A snapshot slower to
upload than that budget never moved. And because `DetachActor` cancelled
atelet's report, nothing corrected the record if the caller failed to
write it.

Now the detach runs on the tracker's context and always makes atelet's
report due.

```mermaid
sequenceDiagram
    participant A as ate-api-server
    participant L as atelet
    participant O as object storage
    A->>L: DetachActor
    L->>O: upload snapshot
    Note over A: resume budget runs out
    L-->>A: Canceled
    Note over L: the detach keeps running
    O-->>L: upload done
    L->>L: delete local copies
    L->>A: ReportActorSnapshotState
    A->>A: record the detach
```

- If the detach finishes in time, `DetachActor` returns its state as
before. Otherwise it returns the caller's error and the detach runs on.
A second call waits for it.
- Every detach makes the report due, including when the upload succeeds
and the prune fails.
- The upload timeout from agent-substrate#2831 now bounds a `DetachActor` call's
upload, since the caller's context no longer does.
- atelet's shutdown waits for a detach that outlived its call. Terminate
cancels it and waits, as agent-substrate#2943 does for any detach.

#### API changes

- `Atelet.DetachActor` (atelet internal). The proto comment now says
atelet reports the detach and that the detach outlives the call. Comment
only, with the regenerated `atelet_grpc.pb.go`.

#### Worth knowing

- Each move now sends one redundant report. It gets `Aborted` while the
actor is RESUMING, then `NotFound` once it runs elsewhere, and atelet
drops it.
- agent-substrate#2831's subtest asserted that a `DetachActor` call ignores the upload
timeout. It now asserts the timeout applies.
@igooch
igooch deleted the resume-elsewhere-detach branch October 27, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api-machinery area/scheduling area/storage kind/feature An enhancement / feature request or implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants