opstate,daemon: list in-flight operations via daemon.operations.list
Prerequisite for `banger update`'s preflight, which refuses to swap
binaries while anything is in flight. Today's opstate.Registry
exposes Insert/Get/Prune but no iteration; without a snapshot
accessor the update flow can't tell whether a vm.create is
mid-prepare-work-disk.
* opstate.Registry.List(): returns a freshly-allocated snapshot
of every entry. Mutating the slice doesn't poison the
registry. Pinned by tests covering the snapshot semantics
and the empty case.
* api.OperationSummary / OperationsListResult: a public-shape
record per op. Today the Kind is always "vm.create" — the
field exists so future async kinds (image.pull, kernel.pull)
plug in without an API change.
* Daemon.ListOperations + daemon.operations.list RPC:
walks vmService.createOps and emits OperationSummary entries.
Done ops are included in the snapshot; the update preflight
filters by Done itself.
* dispatch_test's documented-methods list updated.
No behaviour change for existing flows; this is a read-only
addition.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
775525b592
commit
3c0af3a2de
6 changed files with 117 additions and 2 deletions
|
|
@ -174,6 +174,20 @@ type ImageRefParams struct {
|
||||||
IDOrName string `json:"id_or_name"`
|
IDOrName string `json:"id_or_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OperationSummary struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Stage string `json:"stage,omitempty"`
|
||||||
|
Detail string `json:"detail,omitempty"`
|
||||||
|
Done bool `json:"done"`
|
||||||
|
StartedAt time.Time `json:"started_at,omitempty"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperationsListResult struct {
|
||||||
|
Operations []OperationSummary `json:"operations"`
|
||||||
|
}
|
||||||
|
|
||||||
type ImageCachePruneParams struct {
|
type ImageCachePruneParams struct {
|
||||||
DryRun bool `json:"dry_run,omitempty"`
|
DryRun bool `json:"dry_run,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ func noParamHandler[R any](call func(ctx context.Context, d *Daemon) (R, error))
|
||||||
var rpcHandlers = map[string]handler{
|
var rpcHandlers = map[string]handler{
|
||||||
"ping": pingHandler,
|
"ping": pingHandler,
|
||||||
"shutdown": shutdownHandler,
|
"shutdown": shutdownHandler,
|
||||||
|
"daemon.operations.list": noParamHandler(daemonOperationsListDispatch),
|
||||||
|
|
||||||
"vm.create": paramHandler(vmCreateDispatch),
|
"vm.create": paramHandler(vmCreateDispatch),
|
||||||
"vm.create.begin": paramHandler(vmCreateBeginDispatch),
|
"vm.create.begin": paramHandler(vmCreateBeginDispatch),
|
||||||
|
|
@ -214,6 +215,10 @@ func imageCachePruneDispatch(ctx context.Context, d *Daemon, p api.ImageCachePru
|
||||||
return d.img.PruneOCICache(ctx, p)
|
return d.img.PruneOCICache(ctx, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func daemonOperationsListDispatch(ctx context.Context, d *Daemon) (api.OperationsListResult, error) {
|
||||||
|
return d.ListOperations(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
func kernelListDispatch(ctx context.Context, d *Daemon) (api.KernelListResult, error) {
|
func kernelListDispatch(ctx context.Context, d *Daemon) (api.KernelListResult, error) {
|
||||||
return d.img.KernelList(ctx)
|
return d.img.KernelList(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ func TestRPCHandlersMatchDocumentedMethods(t *testing.T) {
|
||||||
"kernel.pull",
|
"kernel.pull",
|
||||||
"kernel.show",
|
"kernel.show",
|
||||||
|
|
||||||
|
"daemon.operations.list",
|
||||||
|
|
||||||
"ping",
|
"ping",
|
||||||
"shutdown",
|
"shutdown",
|
||||||
|
|
||||||
|
|
|
||||||
37
internal/daemon/operations.go
Normal file
37
internal/daemon/operations.go
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"banger/internal/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListOperations returns a snapshot of every async operation tracked
|
||||||
|
// across the daemon's per-kind registries. Today the only kind is
|
||||||
|
// vm.create; future async kinds (image build, kernel pull) will plug
|
||||||
|
// in here.
|
||||||
|
//
|
||||||
|
// The primary consumer is `banger update`'s preflight, which refuses
|
||||||
|
// to swap binaries while anything is in flight. Done operations are
|
||||||
|
// included in the snapshot so an operator running an interactive
|
||||||
|
// `banger ... | jq` can see recently-completed work; the update
|
||||||
|
// preflight filters by Done itself.
|
||||||
|
func (d *Daemon) ListOperations(_ context.Context) (api.OperationsListResult, error) {
|
||||||
|
out := api.OperationsListResult{Operations: []api.OperationSummary{}}
|
||||||
|
if d.vm == nil {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
for _, op := range d.vm.createOps.List() {
|
||||||
|
snap := op.snapshot()
|
||||||
|
out.Operations = append(out.Operations, api.OperationSummary{
|
||||||
|
ID: snap.ID,
|
||||||
|
Kind: "vm.create",
|
||||||
|
Stage: snap.Stage,
|
||||||
|
Detail: snap.Detail,
|
||||||
|
Done: snap.Done,
|
||||||
|
StartedAt: snap.StartedAt,
|
||||||
|
UpdatedAt: snap.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,23 @@ func (r *Registry[T]) Get(id string) (T, bool) {
|
||||||
return op, ok
|
return op, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns a snapshot of every operation currently in the
|
||||||
|
// registry — both pending and (un-pruned) completed. Callers filter
|
||||||
|
// by IsDone() if they care about state. The slice is freshly
|
||||||
|
// allocated; mutating it doesn't affect the registry.
|
||||||
|
//
|
||||||
|
// Used by `banger update`'s preflight to detect in-flight operations
|
||||||
|
// before swapping binaries.
|
||||||
|
func (r *Registry[T]) List() []T {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
out := make([]T, 0, len(r.byID))
|
||||||
|
for _, op := range r.byID {
|
||||||
|
out = append(out, op)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// Prune drops completed operations last updated before the cutoff.
|
// Prune drops completed operations last updated before the cutoff.
|
||||||
func (r *Registry[T]) Prune(before time.Time) {
|
func (r *Registry[T]) Prune(before time.Time) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,46 @@ func TestRegistryPruneDropsCompletedOldOps(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRegistryListReturnsSnapshot(t *testing.T) {
|
||||||
|
var r Registry[*fakeOp]
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
a := &fakeOp{id: "a", updatedAt: now}
|
||||||
|
b := &fakeOp{id: "b", updatedAt: now}
|
||||||
|
c := &fakeOp{id: "c", updatedAt: now}
|
||||||
|
c.done.Store(true)
|
||||||
|
r.Insert(a)
|
||||||
|
r.Insert(b)
|
||||||
|
r.Insert(c)
|
||||||
|
|
||||||
|
got := r.List()
|
||||||
|
if len(got) != 3 {
|
||||||
|
t.Fatalf("List() returned %d entries, want 3", len(got))
|
||||||
|
}
|
||||||
|
ids := map[string]bool{}
|
||||||
|
for _, op := range got {
|
||||||
|
ids[op.ID()] = true
|
||||||
|
}
|
||||||
|
for _, want := range []string{"a", "b", "c"} {
|
||||||
|
if !ids[want] {
|
||||||
|
t.Errorf("List() missing %q; got %v", want, ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutating the returned slice must not poison the registry.
|
||||||
|
got[0] = &fakeOp{id: "tampered"}
|
||||||
|
if _, ok := r.Get("tampered"); ok {
|
||||||
|
t.Error("List() returned the registry's internal map, not a copy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryListEmpty(t *testing.T) {
|
||||||
|
var r Registry[*fakeOp]
|
||||||
|
if got := r.List(); len(got) != 0 {
|
||||||
|
t.Fatalf("List() on empty registry returned %d entries, want 0", len(got))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRegistryPruneNoOpOnEmpty(t *testing.T) {
|
func TestRegistryPruneNoOpOnEmpty(t *testing.T) {
|
||||||
var r Registry[*fakeOp]
|
var r Registry[*fakeOp]
|
||||||
// Just shouldn't panic.
|
// Just shouldn't panic.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue