From 79df5a45739a919151969d2fdfd0e009381e37db Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 11 May 2026 18:56:32 +0000 Subject: [PATCH 1/8] Track per-phase duration for each instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a small phasetracking package that records cumulative time spent in each lifecycle phase (running, standby, paused, etc.) using transition bookkeeping. Tracker state is persisted with the instance's stored metadata, so it survives process restarts without a DB migration. Instrument transitions directly in create/start/stop/standby/restore/fork rather than subscribing to the lifecycle event stream — the subscription is lossy, and these numbers will feed billing. Expose current_phase, current_phase_since, and phase_durations_ms on the Instance API so callers (notably the kernel-api billing pipeline) can compute true running time instead of wall-clock since CreatedAt. --- cmd/api/api/instances.go | 16 + cmd/api/api/instances_test.go | 59 ++ lib/instances/create.go | 3 + lib/instances/fork.go | 12 + lib/instances/phasetracking/phasetracking.go | 91 +++ .../phasetracking/phasetracking_test.go | 174 ++++++ lib/instances/restore.go | 2 + lib/instances/standby.go | 2 + lib/instances/start.go | 2 + lib/instances/stop.go | 2 + lib/instances/types.go | 7 + lib/oapi/oapi.go | 552 +++++++++--------- openapi.yaml | 26 +- 13 files changed, 680 insertions(+), 268 deletions(-) create mode 100644 lib/instances/phasetracking/phasetracking.go create mode 100644 lib/instances/phasetracking/phasetracking_test.go diff --git a/cmd/api/api/instances.go b/cmd/api/api/instances.go index 61c38ff8..4b3d338c 100644 --- a/cmd/api/api/instances.go +++ b/cmd/api/api/instances.go @@ -1116,6 +1116,22 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance { oapiInst.Gpu = gpu } + // Expose phase accounting (cumulative time in each lifecycle phase). The + // snapshot rolls in time accrued in the current phase since the last + // transition, so consumers don't need a separate "live since" calculation. + if inst.Phases.Current != "" { + current := string(inst.Phases.Current) + oapiInst.CurrentPhase = ¤t + since := inst.Phases.Since + oapiInst.CurrentPhaseSince = &since + snapshot := inst.Phases.Snapshot(time.Now()) + out := make(map[string]int64, len(snapshot)) + for k, v := range snapshot { + out[string(k)] = v + } + oapiInst.PhaseDurationsMs = &out + } + return oapiInst } diff --git a/cmd/api/api/instances_test.go b/cmd/api/api/instances_test.go index 7a664db8..039aa917 100644 --- a/cmd/api/api/instances_test.go +++ b/cmd/api/api/instances_test.go @@ -11,6 +11,7 @@ import ( "github.com/kernel/hypeman/lib/autostandby" "github.com/kernel/hypeman/lib/hypervisor" "github.com/kernel/hypeman/lib/instances" + "github.com/kernel/hypeman/lib/instances/phasetracking" mw "github.com/kernel/hypeman/lib/middleware" "github.com/kernel/hypeman/lib/oapi" "github.com/kernel/hypeman/lib/paths" @@ -531,6 +532,64 @@ func TestCreateInstance_InvalidStandbyCompressionDelayInSnapshotPolicy(t *testin assert.Contains(t, badReq.Message, "standby_compression_delay") } +func TestInstanceToOAPI_EmitsPhaseAccounting(t *testing.T) { + t.Parallel() + + t0 := time.Now().Add(-10 * time.Minute) + tr := phasetracking.Tracker{} + tr.Record(phasetracking.PhaseRunning, t0) + tr.Record(phasetracking.PhaseStandby, t0.Add(60*time.Second)) + tr.Record(phasetracking.PhaseRunning, t0.Add(60*time.Second+5*time.Minute)) + + inst := instances.Instance{ + StoredMetadata: instances.StoredMetadata{ + Id: "inst-phases", + Name: "inst-phases", + Image: "docker.io/library/alpine:latest", + CreatedAt: t0, + HypervisorType: hypervisor.TypeCloudHypervisor, + Phases: tr, + }, + State: instances.StateRunning, + } + + oapiInst := instanceToOAPI(inst) + + require.NotNil(t, oapiInst.CurrentPhase) + assert.Equal(t, "running", *oapiInst.CurrentPhase) + require.NotNil(t, oapiInst.CurrentPhaseSince) + require.NotNil(t, oapiInst.PhaseDurationsMs) + + durations := *oapiInst.PhaseDurationsMs + // Standby stint was a completed 300s window — no live accrual since. + assert.Equal(t, int64(300_000), durations["standby"]) + // Running = 60s completed + live time since latest Record. The + // recorded-at instant is in the past, so this must be >= 60s. + assert.GreaterOrEqual(t, durations["running"], int64(60_000), + "running should include the completed 60s stint") +} + +func TestInstanceToOAPI_OmitsPhaseFieldsWhenUnset(t *testing.T) { + t.Parallel() + + inst := instances.Instance{ + StoredMetadata: instances.StoredMetadata{ + Id: "inst-no-phases", + Name: "inst-no-phases", + Image: "docker.io/library/alpine:latest", + CreatedAt: time.Now(), + HypervisorType: hypervisor.TypeCloudHypervisor, + }, + State: instances.StateStopped, + } + + oapiInst := instanceToOAPI(inst) + + assert.Nil(t, oapiInst.CurrentPhase) + assert.Nil(t, oapiInst.CurrentPhaseSince) + assert.Nil(t, oapiInst.PhaseDurationsMs) +} + func TestInstanceToOAPI_EmitsStandbyCompressionDelayInSnapshotPolicy(t *testing.T) { t.Parallel() diff --git a/lib/instances/create.go b/lib/instances/create.go index b007fc82..2cf2559e 100644 --- a/lib/instances/create.go +++ b/lib/instances/create.go @@ -13,6 +13,7 @@ import ( "github.com/kernel/hypeman/lib/guestmemory" "github.com/kernel/hypeman/lib/hypervisor" "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/logger" "github.com/kernel/hypeman/lib/network" "github.com/kernel/hypeman/lib/system" @@ -490,6 +491,7 @@ func (m *manager) createInstance( } bootStart := time.Now().UTC() stored.StartedAt = &bootStart + stored.Phases.Record(phasetracking.PhaseCreated, bootStart) // 18. Save metadata log.DebugContext(ctx, "saving instance metadata", "instance_id", id) @@ -523,6 +525,7 @@ func (m *manager) createInstance( } // 20. Persist runtime metadata updates after VM boot. + stored.Phases.Record(phasetracking.PhaseRunning, time.Now().UTC()) meta = &metadata{StoredMetadata: *stored} if err := m.saveMetadata(meta); err != nil { // VM is running but metadata failed - log but don't fail diff --git a/lib/instances/fork.go b/lib/instances/fork.go index 4ce7ee6e..7514cd9d 100644 --- a/lib/instances/fork.go +++ b/lib/instances/fork.go @@ -13,6 +13,7 @@ import ( "github.com/kernel/hypeman/lib/forkvm" "github.com/kernel/hypeman/lib/guest" "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/logger" "github.com/kernel/hypeman/lib/network" "github.com/nrednav/cuid2" @@ -280,6 +281,17 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin forkMeta.VsockSocket = m.paths.InstanceSocket(forkID, hypervisor.VsockSocketNameForType(forkMeta.HypervisorType)) forkMeta.ExitCode = nil forkMeta.ExitMessage = "" + // Forks are new instances; phase accounting must not inherit the source's + // cumulative durations. The first transition into the fork's runtime + // phase (Standby for snapshot forks, Stopped for stopped forks) will be + // recorded by the appropriate operation when the fork is acted on. + forkMeta.Phases.Reset() + switch source.State { + case StateStandby: + forkMeta.Phases.Record(phasetracking.PhaseStandby, now) + case StateStopped: + forkMeta.Phases.Record(phasetracking.PhaseStopped, now) + } // Keep the original CID for snapshot-based forks. // Rewriting CID in restored memory snapshots is not reliable across diff --git a/lib/instances/phasetracking/phasetracking.go b/lib/instances/phasetracking/phasetracking.go new file mode 100644 index 00000000..d42490d2 --- /dev/null +++ b/lib/instances/phasetracking/phasetracking.go @@ -0,0 +1,91 @@ +// Package phasetracking accumulates cumulative time-in-phase for instance +// lifecycle phases (running, standby, paused, etc.). The tracker is embedded +// in instance metadata and updated at every state transition. Consumers use +// the resulting durations for billing, observability, and analytics. +// +// Only the transition orchestration sites in lib/instances should call Record. +// The tracker intentionally does not subscribe to the lifecycle event bus — +// that bus is best-effort and lossy, which is unsuitable for billing. +package phasetracking + +import "time" + +// Phase is the canonical lifecycle phase name. Values mirror instances.State +// lowercased so they remain stable in the API surface even if the internal +// State enum is renamed. +type Phase string + +const ( + PhaseStopped Phase = "stopped" + PhaseCreated Phase = "created" + PhaseInitializing Phase = "initializing" + PhaseRunning Phase = "running" + PhasePaused Phase = "paused" + PhaseShutdown Phase = "shutdown" + PhaseStandby Phase = "standby" +) + +// Tracker accumulates cumulative wall-clock time spent in each phase. +// +// Invariants: +// - Cumulative[phase] is the total ms spent in `phase` across all prior +// completed visits to that phase. +// - Time spent in the *current* phase (since `Since`) is NOT yet rolled into +// Cumulative — callers that want "live" totals should use Snapshot. +// - Current and Since must be updated atomically with Cumulative; that's +// the contract of Record. Direct mutation is not supported. +// +// The zero value is valid: it represents an instance that has not entered +// any phase yet. The first Record call sets Current and Since without +// accruing time (there is no prior phase to accrue from). +type Tracker struct { + Current Phase `json:"current,omitempty"` + Since time.Time `json:"since,omitempty"` + Cumulative map[Phase]int64 `json:"cumulative,omitempty"` +} + +// Record transitions into newPhase as of `now`, first accruing time-in-current +// into Cumulative. Safe to call on a zero-value Tracker (first transition has +// no prior phase, so no accrual happens). +// +// `now` is a parameter rather than time.Now() so tests can pin time and so +// callers can use the same `now` value they're persisting elsewhere on the +// metadata (e.g. StartedAt) without drift. +func (t *Tracker) Record(newPhase Phase, now time.Time) { + if t.Cumulative == nil { + t.Cumulative = make(map[Phase]int64) + } + if t.Current != "" && !t.Since.IsZero() { + elapsed := now.Sub(t.Since).Milliseconds() + if elapsed > 0 { + t.Cumulative[t.Current] += elapsed + } + } + t.Current = newPhase + t.Since = now +} + +// Snapshot returns a copy of Cumulative with the in-flight time-in-current +// rolled in, without mutating the tracker. Use this when reporting "running +// time so far" — typically in the API response path. +func (t Tracker) Snapshot(now time.Time) map[Phase]int64 { + out := make(map[Phase]int64, len(t.Cumulative)+1) + for k, v := range t.Cumulative { + out[k] = v + } + if t.Current != "" && !t.Since.IsZero() { + elapsed := now.Sub(t.Since).Milliseconds() + if elapsed > 0 { + out[t.Current] += elapsed + } + } + return out +} + +// Reset clears all accumulated state. Used when forking — the fork is a new +// instance and must not inherit the source's phase history. +func (t *Tracker) Reset() { + t.Current = "" + t.Since = time.Time{} + t.Cumulative = nil +} diff --git a/lib/instances/phasetracking/phasetracking_test.go b/lib/instances/phasetracking/phasetracking_test.go new file mode 100644 index 00000000..bc2d7f5e --- /dev/null +++ b/lib/instances/phasetracking/phasetracking_test.go @@ -0,0 +1,174 @@ +package phasetracking + +import ( + "encoding/json" + "testing" + "time" +) + +func TestRecord_FirstTransitionAccruesNothing(t *testing.T) { + var tr Tracker + now := time.Date(2026, 5, 11, 0, 0, 0, 0, time.UTC) + + tr.Record(PhaseRunning, now) + + if tr.Current != PhaseRunning { + t.Errorf("Current = %q, want %q", tr.Current, PhaseRunning) + } + if !tr.Since.Equal(now) { + t.Errorf("Since = %v, want %v", tr.Since, now) + } + if got := tr.Cumulative[PhaseRunning]; got != 0 { + t.Errorf("Cumulative[running] = %d, want 0 on first transition", got) + } +} + +func TestRecord_AccruesPriorPhaseOnTransition(t *testing.T) { + var tr Tracker + t0 := time.Date(2026, 5, 11, 0, 0, 0, 0, time.UTC) + + tr.Record(PhaseRunning, t0) + tr.Record(PhaseStandby, t0.Add(30*time.Second)) + tr.Record(PhaseRunning, t0.Add(30*time.Second+5*time.Minute)) + tr.Record(PhaseStopped, t0.Add(30*time.Second+5*time.Minute+10*time.Second)) + + if got, want := tr.Cumulative[PhaseRunning], int64(40_000); got != want { + t.Errorf("Cumulative[running] = %d, want %d", got, want) + } + if got, want := tr.Cumulative[PhaseStandby], int64(300_000); got != want { + t.Errorf("Cumulative[standby] = %d, want %d", got, want) + } + if tr.Current != PhaseStopped { + t.Errorf("Current = %q, want %q", tr.Current, PhaseStopped) + } +} + +func TestRecord_ZeroOrNegativeElapsedIsNoOp(t *testing.T) { + var tr Tracker + t0 := time.Date(2026, 5, 11, 0, 0, 0, 0, time.UTC) + + tr.Record(PhaseRunning, t0) + // Same instant — no time elapsed. + tr.Record(PhaseStandby, t0) + // Backward clock — also no accrual. + tr.Record(PhaseRunning, t0.Add(-5*time.Second)) + + if got := tr.Cumulative[PhaseRunning]; got != 0 { + t.Errorf("Cumulative[running] = %d, want 0 (zero/negative elapsed)", got) + } + if got := tr.Cumulative[PhaseStandby]; got != 0 { + t.Errorf("Cumulative[standby] = %d, want 0 (zero/negative elapsed)", got) + } +} + +func TestSnapshot_IncludesLiveTime(t *testing.T) { + var tr Tracker + t0 := time.Date(2026, 5, 11, 0, 0, 0, 0, time.UTC) + + tr.Record(PhaseRunning, t0) + tr.Record(PhaseStandby, t0.Add(60*time.Second)) + + // Now we're 10s into standby. Cumulative shouldn't yet include this. + live := tr.Snapshot(t0.Add(70 * time.Second)) + + if got, want := tr.Cumulative[PhaseRunning], int64(60_000); got != want { + t.Errorf("Cumulative[running] = %d, want %d", got, want) + } + if _, present := tr.Cumulative[PhaseStandby]; present { + t.Errorf("Cumulative should not yet contain standby") + } + if got, want := live[PhaseRunning], int64(60_000); got != want { + t.Errorf("Snapshot[running] = %d, want %d", got, want) + } + if got, want := live[PhaseStandby], int64(10_000); got != want { + t.Errorf("Snapshot[standby] = %d, want %d", got, want) + } +} + +func TestSnapshot_DoesNotMutate(t *testing.T) { + var tr Tracker + t0 := time.Date(2026, 5, 11, 0, 0, 0, 0, time.UTC) + tr.Record(PhaseRunning, t0) + + _ = tr.Snapshot(t0.Add(5 * time.Minute)) + + if got := tr.Cumulative[PhaseRunning]; got != 0 { + t.Errorf("Cumulative[running] mutated by Snapshot: got %d, want 0", got) + } +} + +func TestReset_ClearsAll(t *testing.T) { + var tr Tracker + t0 := time.Date(2026, 5, 11, 0, 0, 0, 0, time.UTC) + tr.Record(PhaseRunning, t0) + tr.Record(PhaseStandby, t0.Add(time.Minute)) + + tr.Reset() + + if tr.Current != "" || !tr.Since.IsZero() || tr.Cumulative != nil { + t.Errorf("Reset did not clear all fields: %+v", tr) + } +} + +func TestJSONRoundTrip_PreservesTracker(t *testing.T) { + t0 := time.Date(2026, 5, 11, 0, 0, 0, 0, time.UTC) + tr := Tracker{ + Current: PhaseStandby, + Since: t0, + Cumulative: map[Phase]int64{ + PhaseRunning: 12_345, + PhaseStandby: 67_890, + }, + } + + data, err := json.Marshal(tr) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got Tracker + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if got.Current != tr.Current || !got.Since.Equal(tr.Since) { + t.Errorf("roundtrip current/since mismatch: %+v", got) + } + if got.Cumulative[PhaseRunning] != 12_345 || got.Cumulative[PhaseStandby] != 67_890 { + t.Errorf("roundtrip cumulative mismatch: %+v", got.Cumulative) + } +} + +func TestJSONRoundTrip_ZeroValueOmitted(t *testing.T) { + // A fresh metadata file written before this feature shipped will not + // contain the `phases` field. Unmarshalling must succeed and produce a + // zero-value Tracker so the first Record call works as a fresh start. + var tr Tracker + if err := json.Unmarshal([]byte(`{}`), &tr); err != nil { + t.Fatalf("unmarshal empty: %v", err) + } + tr.Record(PhaseRunning, time.Now()) + if tr.Current != PhaseRunning { + t.Errorf("Current after fresh Record = %q, want running", tr.Current) + } +} + +// Regression: a session that spends 60s running then 300s in standby then +// 30s running again must report 90s running and 300s standby for billing. +func TestRecord_BillingScenario(t *testing.T) { + var tr Tracker + t0 := time.Date(2026, 5, 11, 0, 0, 0, 0, time.UTC) + + tr.Record(PhaseRunning, t0) + tr.Record(PhaseStandby, t0.Add(60*time.Second)) + tr.Record(PhaseRunning, t0.Add(60*time.Second+300*time.Second)) + tr.Record(PhaseStopped, t0.Add(60*time.Second+300*time.Second+30*time.Second)) + + billableRunning := tr.Cumulative[PhaseRunning] + standby := tr.Cumulative[PhaseStandby] + + if billableRunning != 90_000 { + t.Errorf("billable running ms = %d, want 90000", billableRunning) + } + if standby != 300_000 { + t.Errorf("standby ms = %d, want 300000", standby) + } +} diff --git a/lib/instances/restore.go b/lib/instances/restore.go index 268f769b..12148b1a 100644 --- a/lib/instances/restore.go +++ b/lib/instances/restore.go @@ -12,6 +12,7 @@ import ( "github.com/kernel/hypeman/lib/guest" "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/logger" "github.com/kernel/hypeman/lib/network" snapshotstore "github.com/kernel/hypeman/lib/snapshot" @@ -307,6 +308,7 @@ func (m *manager) restoreInstance( // 9. Persist runtime metadata updates without resetting StartedAt. // Restore resumes an existing boot; preserving StartedAt keeps marker // hydration scoped to the original boot timeline. + stored.Phases.Record(phasetracking.PhaseRunning, time.Now().UTC()) meta = &metadata{StoredMetadata: *stored} if err := m.saveMetadata(meta); err != nil { // VM is running but metadata failed diff --git a/lib/instances/standby.go b/lib/instances/standby.go index d9de8053..a23ee892 100644 --- a/lib/instances/standby.go +++ b/lib/instances/standby.go @@ -11,6 +11,7 @@ import ( "github.com/kernel/hypeman/lib/guest" "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/logger" "github.com/kernel/hypeman/lib/network" snapshotstore "github.com/kernel/hypeman/lib/snapshot" @@ -221,6 +222,7 @@ func (m *manager) standbyInstance( NotBefore: m.nowUTC().Add(compressionDelay), } } + stored.Phases.Record(phasetracking.PhaseStandby, now) meta = &metadata{StoredMetadata: *stored} if err := m.saveMetadata(meta); err != nil { diff --git a/lib/instances/start.go b/lib/instances/start.go index 8da3026e..a3fcd569 100644 --- a/lib/instances/start.go +++ b/lib/instances/start.go @@ -7,6 +7,7 @@ import ( "github.com/kernel/hypeman/lib/devices" "github.com/kernel/hypeman/lib/egressproxy" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/logger" "github.com/kernel/hypeman/lib/network" "go.opentelemetry.io/otel/attribute" @@ -210,6 +211,7 @@ func (m *manager) startInstance( cu.Release() // 7. Update metadata (set PID, StartedAt) + stored.Phases.Record(phasetracking.PhaseRunning, time.Now().UTC()) meta = &metadata{StoredMetadata: *stored} if err := m.saveMetadata(meta); err != nil { // VM is running but metadata failed - log but don't fail diff --git a/lib/instances/stop.go b/lib/instances/stop.go index 133e7156..c5f00fda 100644 --- a/lib/instances/stop.go +++ b/lib/instances/stop.go @@ -12,6 +12,7 @@ import ( "github.com/kernel/hypeman/lib/devices" "github.com/kernel/hypeman/lib/guest" "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/logger" "github.com/kernel/hypeman/lib/network" "go.opentelemetry.io/otel/attribute" @@ -288,6 +289,7 @@ func (m *manager) stopInstance( // Boot markers are per-boot-run and must not carry across stop/restore/start. stored.ProgramStartedAt = nil stored.GuestAgentReadyAt = nil + stored.Phases.Record(phasetracking.PhaseStopped, now) meta = &metadata{StoredMetadata: *stored} if err := m.saveMetadata(meta); err != nil { diff --git a/lib/instances/types.go b/lib/instances/types.go index 56243a0d..bf539714 100644 --- a/lib/instances/types.go +++ b/lib/instances/types.go @@ -5,6 +5,7 @@ import ( "github.com/kernel/hypeman/lib/autostandby" "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/snapshot" "github.com/kernel/hypeman/lib/tags" ) @@ -153,6 +154,12 @@ type StoredMetadata struct { // Exit information (populated from serial console sentinel when VM stops) ExitCode *int // App exit code, nil if VM hasn't exited ExitMessage string // Human-readable description of exit (e.g., "command not found", "killed by signal 9 (SIGKILL) - OOM") + + // Cumulative time spent in each lifecycle phase. Updated at every state + // transition by transition orchestration sites (create/start/stop/standby/ + // restore/fork). Consumers use Snapshot() to read live values that include + // time accrued in the current phase. See lib/instances/phasetracking. + Phases phasetracking.Tracker `json:"phases,omitempty"` } // Instance represents a virtual machine instance with derived runtime state diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 093dd8f5..919636aa 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -863,6 +863,12 @@ type Instance struct { // CreatedAt Creation timestamp (RFC3339) CreatedAt time.Time `json:"created_at"` + // CurrentPhase The lifecycle phase the instance is currently in. + CurrentPhase *string `json:"current_phase,omitempty"` + + // CurrentPhaseSince When the instance entered current_phase. + CurrentPhaseSince *time.Time `json:"current_phase_since,omitempty"` + // DiskIoBps Disk I/O rate limit (human-readable, e.g., "100MB/s") DiskIoBps *string `json:"disk_io_bps,omitempty"` @@ -920,6 +926,14 @@ type Instance struct { // OverlaySize Writable overlay disk size (human-readable) OverlaySize *string `json:"overlay_size,omitempty"` + // PhaseDurationsMs Cumulative milliseconds the instance has spent in each lifecycle + // phase, including time accrued in the current phase up to the + // response time. Keys mirror instance states lowercased + // (running, standby, paused, stopped, created, initializing, + // shutdown). Consumers (e.g. billing) sum the phases they consider + // billable. + PhaseDurationsMs *map[string]int64 `json:"phase_durations_ms,omitempty"` + // Size Base memory size (human-readable) Size *string `json:"size,omitempty"` SnapshotPolicy *SnapshotPolicy `json:"snapshot_policy,omitempty"` @@ -15654,273 +15668,277 @@ func (sh *strictHandler) GetVolume(w http.ResponseWriter, r *http.Request, id st // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/openapi.yaml b/openapi.yaml index e6446965..96c6ed61 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -878,7 +878,31 @@ components: $ref: "#/components/schemas/SnapshotPolicy" auto_standby: $ref: "#/components/schemas/AutoStandbyPolicy" - + phase_durations_ms: + type: object + description: | + Cumulative milliseconds the instance has spent in each lifecycle + phase, including time accrued in the current phase up to the + response time. Keys mirror instance states lowercased + (running, standby, paused, stopped, created, initializing, + shutdown). Consumers (e.g. billing) sum the phases they consider + billable. + additionalProperties: + type: integer + format: int64 + example: + running: 60000 + standby: 300000 + current_phase: + type: string + description: The lifecycle phase the instance is currently in. + example: running + current_phase_since: + type: string + format: date-time + description: When the instance entered current_phase. + example: "2026-05-11T14:00:00Z" + PathInfo: type: object required: [exists] From 86dcffe801b070ca761a5289a3d5fdfe910017fb Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 11 May 2026 19:00:33 +0000 Subject: [PATCH 2/8] gofmt phasetracking --- lib/instances/phasetracking/phasetracking.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/instances/phasetracking/phasetracking.go b/lib/instances/phasetracking/phasetracking.go index d42490d2..ad13b9be 100644 --- a/lib/instances/phasetracking/phasetracking.go +++ b/lib/instances/phasetracking/phasetracking.go @@ -39,9 +39,9 @@ const ( // any phase yet. The first Record call sets Current and Since without // accruing time (there is no prior phase to accrue from). type Tracker struct { - Current Phase `json:"current,omitempty"` - Since time.Time `json:"since,omitempty"` - Cumulative map[Phase]int64 `json:"cumulative,omitempty"` + Current Phase `json:"current,omitempty"` + Since time.Time `json:"since,omitempty"` + Cumulative map[Phase]int64 `json:"cumulative,omitempty"` } // Record transitions into newPhase as of `now`, first accruing time-in-current From 76be7f15897213cdce3620fbe9e19c4423065269 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 11 May 2026 19:34:25 +0000 Subject: [PATCH 3/8] Assert phase tracking in existing standby/fork integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Piggyback on the firecracker/QEMU standby-restore cycles and the cloud-hypervisor fork-from-running test to assert end-to-end that transition-site instrumentation is wired up: - after standby: Current == standby, Cumulative[running] > 0 - after restore: Current == running, Cumulative[standby] > 0 - after fork-from-running: fork's Cumulative[running] is zero while source's is non-zero — locks down the Phases.Reset() semantics No new tests, no added sleeps. The assertions read state at the same points where the tests already check State. --- lib/instances/firecracker_test.go | 7 +++++++ lib/instances/fork_test.go | 9 +++++++++ lib/instances/qemu_test.go | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/lib/instances/firecracker_test.go b/lib/instances/firecracker_test.go index 25973930..2ee440b1 100644 --- a/lib/instances/firecracker_test.go +++ b/lib/instances/firecracker_test.go @@ -17,6 +17,7 @@ import ( "github.com/kernel/hypeman/lib/devices" "github.com/kernel/hypeman/lib/hypervisor" "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/network" "github.com/kernel/hypeman/lib/paths" "github.com/kernel/hypeman/lib/resources" @@ -163,6 +164,7 @@ func TestFirecrackerStandbyAndRestore(t *testing.T) { inst, err = waitForInstanceState(ctx, mgr, inst.Id, StateRunning, integrationTestTimeout(20*time.Second)) require.NoError(t, err) require.NoError(t, waitForExecAgent(ctx, mgr, inst.Id, 30*time.Second)) + assert.Equal(t, phasetracking.PhaseRunning, inst.Phases.Current, "fresh instance should be in running phase") firstFilePath := "/tmp/firecracker-standby-first.txt" secondFilePath := "/tmp/firecracker-standby-second.txt" @@ -222,10 +224,14 @@ func TestFirecrackerStandbyAndRestore(t *testing.T) { t.Logf("first standby (full snapshot expected) took %v", firstStandbyDuration) assert.Equal(t, StateStandby, inst.State) assert.True(t, inst.HasSnapshot) + assert.Equal(t, phasetracking.PhaseStandby, inst.Phases.Current, "standby transition should set current phase") + assert.Greater(t, inst.Phases.Cumulative[phasetracking.PhaseRunning], int64(0), "first running stint should be accrued after standby") firstRestoreRunningDuration, _ := restoreAndMeasure("first") assert.False(t, inst.HasSnapshot, "running instances should not expose retained snapshot bases as standby snapshots") assertRetainedBaseState() + assert.Equal(t, phasetracking.PhaseRunning, inst.Phases.Current, "restored instance should be in running phase") + assert.Greater(t, inst.Phases.Cumulative[phasetracking.PhaseStandby], int64(0), "first standby stint should be accrued after restore") t.Logf("first full-cycle timings: standby=%v restore-to-running=%v", firstStandbyDuration, firstRestoreRunningDuration) assertGuestFileContents(firstFilePath, firstFileContents) @@ -245,6 +251,7 @@ func TestFirecrackerStandbyAndRestore(t *testing.T) { secondRestoreRunningDuration, _ := restoreAndMeasure("second") assert.False(t, inst.HasSnapshot, "running instances should not expose retained snapshot bases as standby snapshots") assertRetainedBaseState() + assert.Equal(t, phasetracking.PhaseRunning, inst.Phases.Current, "second restore should land back in running") t.Logf("second diff-cycle timings: standby=%v restore-to-running=%v", secondStandbyDuration, secondRestoreRunningDuration) assertGuestFileContents(secondFilePath, secondFileContents) diff --git a/lib/instances/fork_test.go b/lib/instances/fork_test.go index f73e892b..0dd769e3 100644 --- a/lib/instances/fork_test.go +++ b/lib/instances/fork_test.go @@ -18,6 +18,7 @@ import ( "github.com/kernel/hypeman/lib/guest" "github.com/kernel/hypeman/lib/hypervisor" "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/paths" snapshotstore "github.com/kernel/hypeman/lib/snapshot" "github.com/stretchr/testify/assert" @@ -548,6 +549,14 @@ func TestForkCloudHypervisorFromRunningNetwork(t *testing.T) { assert.NotEqual(t, sourceAfterFork.MAC, forked.MAC) assertGuestHasOnlyExpectedIPv4(t, forked, forked.IP, 30*time.Second) assertHostCanReachNginx(t, forked.IP, 80, 60*time.Second) + + // Fork must start with a fresh phase ledger and not inherit the source's + // accumulated running time. The source has completed at least one running + // stint by now (running -> internal-standby -> running); the fork is still + // in its first running stint and so has no completed running ms logged. + assert.Equal(t, phasetracking.PhaseRunning, forked.Phases.Current) + assert.Zero(t, forked.Phases.Cumulative[phasetracking.PhaseRunning], "fork should not inherit source's running ledger") + assert.Greater(t, sourceAfterFork.Phases.Cumulative[phasetracking.PhaseRunning], int64(0), "source's pre-fork running stint should be cumulated") } func assertHostCanReachNginx(t *testing.T, ip string, port int, timeout time.Duration) { diff --git a/lib/instances/qemu_test.go b/lib/instances/qemu_test.go index 1695b3b8..015f8078 100644 --- a/lib/instances/qemu_test.go +++ b/lib/instances/qemu_test.go @@ -22,6 +22,7 @@ import ( "github.com/kernel/hypeman/lib/hypervisor/qemu" "github.com/kernel/hypeman/lib/images" "github.com/kernel/hypeman/lib/ingress" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/network" "github.com/kernel/hypeman/lib/paths" "github.com/kernel/hypeman/lib/resources" @@ -838,6 +839,7 @@ func TestQEMUStandbyAndRestore(t *testing.T) { // Wait for VM to be fully running before standby err = waitForQEMUReady(ctx, inst.SocketPath, 10*time.Second) require.NoError(t, err, "QEMU VM should reach running state") + assert.Equal(t, phasetracking.PhaseRunning, inst.Phases.Current, "fresh instance should be in running phase") // Standby instance t.Log("Standing by instance...") @@ -845,6 +847,8 @@ func TestQEMUStandbyAndRestore(t *testing.T) { require.NoError(t, err) assert.Equal(t, StateStandby, inst.State) assert.True(t, inst.HasSnapshot) + assert.Equal(t, phasetracking.PhaseStandby, inst.Phases.Current, "standby transition should set current phase") + assert.Greater(t, inst.Phases.Cumulative[phasetracking.PhaseRunning], int64(0), "running stint should be accrued after standby") t.Log("Instance in standby") // Verify snapshot exists @@ -873,6 +877,8 @@ func TestQEMUStandbyAndRestore(t *testing.T) { // Wait for VM to be running again err = waitForQEMUReady(ctx, inst.SocketPath, 10*time.Second) require.NoError(t, err, "QEMU VM should reach running state after restore") + assert.Equal(t, phasetracking.PhaseRunning, inst.Phases.Current, "restored instance should be in running phase") + assert.Greater(t, inst.Phases.Cumulative[phasetracking.PhaseStandby], int64(0), "standby stint should be accrued after restore") // Cleanup t.Log("Cleaning up...") From 7a805211a7334b6d1590251afc4aec38f8cb04c0 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 11 May 2026 19:59:36 +0000 Subject: [PATCH 4/8] Use UTC for now in standby/stop transition timestamps The phase tracker's Since field is persisted and exposed in the API as current_phase_since. standby/stop were initializing `now` as local time while create/start/restore use UTC, leaving downstream consumers with mixed timezone offsets in the serialized value depending on which transition last occurred. Align all transition sites on UTC. StoppedAt moves to UTC as a byproduct, which is the correct normalization anyway. --- lib/instances/standby.go | 2 +- lib/instances/stop.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/instances/standby.go b/lib/instances/standby.go index a23ee892..03b27410 100644 --- a/lib/instances/standby.go +++ b/lib/instances/standby.go @@ -212,7 +212,7 @@ func (m *manager) standbyInstance( } // 10. Update timestamp and clear PID (hypervisor no longer running) - now := time.Now() + now := time.Now().UTC() stored.StoppedAt = &now stored.HypervisorPID = nil stored.PendingStandbyCompression = nil diff --git a/lib/instances/stop.go b/lib/instances/stop.go index c5f00fda..1d67c03b 100644 --- a/lib/instances/stop.go +++ b/lib/instances/stop.go @@ -282,7 +282,7 @@ func (m *manager) stopInstance( } // 10. Update metadata (clear PID, mdev UUID, set StoppedAt) - now := time.Now() + now := time.Now().UTC() stored.StoppedAt = &now stored.HypervisorPID = nil stored.GPUMdevUUID = "" // Clear mdev UUID since we destroyed it From a6f4d1094d908fd0739eaee71531ea3b5f839fd2 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 11 May 2026 20:08:44 +0000 Subject: [PATCH 5/8] Deep-copy phase tracker on metadata clone and use UTC in fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cloneStoredMetadata previously shallow-copied the Phases tracker, which aliased the Cumulative map between source and forked metadata — a subsequent Record on either side would mutate both. Add Tracker.Clone and use it from cloneStoredMetadata. Also normalise the fork transition timestamp to UTC for consistency with the other transition sites. --- lib/instances/fork.go | 3 +- lib/instances/phasetracking/phasetracking.go | 13 +++++ .../phasetracking/phasetracking_test.go | 49 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/lib/instances/fork.go b/lib/instances/fork.go index 7514cd9d..d3f837dc 100644 --- a/lib/instances/fork.go +++ b/lib/instances/fork.go @@ -268,7 +268,7 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin return nil, fmt.Errorf("get vm starter: %w", err) } - now := time.Now() + now := time.Now().UTC() forkMeta := cloneStoredMetadataWithoutPendingStandbyCompression(meta.StoredMetadata) forkMeta.Id = forkID forkMeta.Name = req.Name @@ -526,6 +526,7 @@ func cloneStoredMetadata(src StoredMetadata) StoredMetadata { exitCode := *src.ExitCode dst.ExitCode = &exitCode } + dst.Phases = src.Phases.Clone() return dst } diff --git a/lib/instances/phasetracking/phasetracking.go b/lib/instances/phasetracking/phasetracking.go index ad13b9be..8502a438 100644 --- a/lib/instances/phasetracking/phasetracking.go +++ b/lib/instances/phasetracking/phasetracking.go @@ -89,3 +89,16 @@ func (t *Tracker) Reset() { t.Since = time.Time{} t.Cumulative = nil } + +// Clone returns a deep copy of the tracker. The returned tracker shares no +// state with the receiver, so independent Record/Reset calls do not interfere. +func (t Tracker) Clone() Tracker { + dst := t + if t.Cumulative != nil { + dst.Cumulative = make(map[Phase]int64, len(t.Cumulative)) + for k, v := range t.Cumulative { + dst.Cumulative[k] = v + } + } + return dst +} diff --git a/lib/instances/phasetracking/phasetracking_test.go b/lib/instances/phasetracking/phasetracking_test.go index bc2d7f5e..84c7ed62 100644 --- a/lib/instances/phasetracking/phasetracking_test.go +++ b/lib/instances/phasetracking/phasetracking_test.go @@ -151,6 +151,55 @@ func TestJSONRoundTrip_ZeroValueOmitted(t *testing.T) { } } +// Regression: cloneStoredMetadata used to shallow-copy the embedded Tracker, +// which aliased the Cumulative map between source and destination. A +// subsequent Record on either side mutated both. Clone must produce a +// fully independent tracker. +func TestClone_IsDeepCopy(t *testing.T) { + t0 := time.Date(2026, 5, 11, 0, 0, 0, 0, time.UTC) + src := Tracker{ + Current: PhaseRunning, + Since: t0, + Cumulative: map[Phase]int64{ + PhaseRunning: 1_000, + PhaseStandby: 2_000, + }, + } + + dst := src.Clone() + + // Mutating the clone must not affect the source. + dst.Record(PhaseStandby, t0.Add(5*time.Second)) + if got, want := src.Cumulative[PhaseRunning], int64(1_000); got != want { + t.Errorf("source running mutated by clone.Record: got %d, want %d", got, want) + } + if src.Current != PhaseRunning { + t.Errorf("source Current mutated by clone.Record: got %q, want %q", src.Current, PhaseRunning) + } + + // And vice-versa: mutating the source must not affect the clone. + src.Record(PhaseStopped, t0.Add(10*time.Second)) + if dst.Current != PhaseStandby { + t.Errorf("clone Current mutated by src.Record: got %q, want %q", dst.Current, PhaseStandby) + } + if got := dst.Cumulative[PhaseStopped]; got != 0 { + t.Errorf("clone cumulative leaked from src.Record: got %d, want 0", got) + } +} + +func TestClone_ZeroValueSafe(t *testing.T) { + var src Tracker + dst := src.Clone() + if dst.Current != "" || !dst.Since.IsZero() || dst.Cumulative != nil { + t.Errorf("clone of zero value is not zero: %+v", dst) + } + // Recording on the clone must not panic and must not touch source. + dst.Record(PhaseRunning, time.Now()) + if src.Current != "" || src.Cumulative != nil { + t.Errorf("zero source mutated by clone.Record: %+v", src) + } +} + // Regression: a session that spends 60s running then 300s in standby then // 30s running again must report 90s running and 300s standby for billing. func TestRecord_BillingScenario(t *testing.T) { From c535c06b88f1edaab68aae10588ea9c4a3e2902f Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 11 May 2026 21:19:27 +0000 Subject: [PATCH 6/8] Mirror public State in phase tracking via lazy boot-marker hydration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recording sites previously jumped straight to PhaseRunning the moment the VMM was up, but the public State machine stays in Initializing until both ProgramStartedAt and GuestAgentReadyAt are hydrated from the guest serial log. That meant Phases.Current reported "running" while the API reported "Initializing". Make phase tracking honest: - create/start record PhaseInitializing on VM boot - restore inspects the preserved markers and records whichever phase the guest is actually in (Running in the common case) - hydrateBootMarkersFromLogs / persistBootMarkers detect the Initializing → Running boundary and Record(PhaseRunning) using the later marker timestamp, so the accrued Initializing duration matches real guest boot time rather than the wall clock when hydration ran Transient internal substates (Paused/Shutdown inside Standby/Stop) remain unrecorded — they're sub-ms blips inside non-yielding orchestration that no external observer can see. --- lib/instances/create.go | 7 +- lib/instances/phasetracking/phasetracking.go | 18 +- lib/instances/query.go | 48 ++++++ lib/instances/query_test.go | 167 +++++++++++++++++++ lib/instances/restore.go | 9 +- lib/instances/start.go | 5 +- 6 files changed, 244 insertions(+), 10 deletions(-) diff --git a/lib/instances/create.go b/lib/instances/create.go index 2cf2559e..988aef34 100644 --- a/lib/instances/create.go +++ b/lib/instances/create.go @@ -524,8 +524,11 @@ func (m *manager) createInstance( reservedResources = false } - // 20. Persist runtime metadata updates after VM boot. - stored.Phases.Record(phasetracking.PhaseRunning, time.Now().UTC()) + // 20. Persist runtime metadata updates after VM boot. The VMM is up but + // guest boot markers have not yet been written, so we are in Initializing; + // persistBootMarkers will advance us to Running once the markers appear + // in the serial log. + stored.Phases.Record(phasetracking.PhaseInitializing, time.Now().UTC()) meta = &metadata{StoredMetadata: *stored} if err := m.saveMetadata(meta); err != nil { // VM is running but metadata failed - log but don't fail diff --git a/lib/instances/phasetracking/phasetracking.go b/lib/instances/phasetracking/phasetracking.go index 8502a438..10261cf7 100644 --- a/lib/instances/phasetracking/phasetracking.go +++ b/lib/instances/phasetracking/phasetracking.go @@ -1,7 +1,19 @@ // Package phasetracking accumulates cumulative time-in-phase for instance -// lifecycle phases (running, standby, paused, etc.). The tracker is embedded -// in instance metadata and updated at every state transition. Consumers use -// the resulting durations for billing, observability, and analytics. +// lifecycle phases. The tracker is embedded in instance metadata and updated +// at every externally-observable state transition so consumers can use the +// resulting durations for billing, observability, and analytics. +// +// Phases mirror the externally-observable values of instances.State (lowercased +// so they remain stable in the API surface even if the internal enum is +// renamed). The Initializing→Running transition is detected lazily when guest +// boot markers are persisted, so the tracker reflects the same view of guest +// readiness that the public State machine reports — not the bare moment the +// VMM process came up. +// +// Transient internal substates that no external observer can see — for example +// the Paused/Shutdown steps inside a single Standby or Stop orchestration — are +// intentionally not recorded; they would be sub-millisecond blips inside a +// non-yielding function call that adds noise without truth. // // Only the transition orchestration sites in lib/instances should call Record. // The tracker intentionally does not subscribe to the lifecycle event bus — diff --git a/lib/instances/query.go b/lib/instances/query.go index 699fa35f..58add880 100644 --- a/lib/instances/query.go +++ b/lib/instances/query.go @@ -15,6 +15,7 @@ import ( "time" "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/logger" "go.opentelemetry.io/otel/attribute" ) @@ -128,6 +129,50 @@ func deriveRunningState(stored *StoredMetadata) State { return StateRunning } +// runningPhaseFromMarkers returns PhaseRunning with the wall-clock timestamp at +// which the guest crossed the Initializing→Running boundary, derived from the +// boot markers. If the markers do not yet indicate a Running guest, it returns +// PhaseInitializing and the zero time. The returned transition time is the +// later of ProgramStartedAt / GuestAgentReadyAt (or ProgramStartedAt alone +// when the guest agent is skipped), which is the moment the guest actually +// became Running per deriveRunningState's rule. +func runningPhaseFromMarkers(stored *StoredMetadata) (phasetracking.Phase, time.Time) { + if stored.ProgramStartedAt == nil { + return phasetracking.PhaseInitializing, time.Time{} + } + if stored.SkipGuestAgent { + return phasetracking.PhaseRunning, *stored.ProgramStartedAt + } + if stored.GuestAgentReadyAt == nil { + return phasetracking.PhaseInitializing, time.Time{} + } + transition := *stored.ProgramStartedAt + if stored.GuestAgentReadyAt.After(transition) { + transition = *stored.GuestAgentReadyAt + } + return phasetracking.PhaseRunning, transition +} + +// advancePhaseIfRunning promotes stored.Phases from Initializing to Running +// when the boot markers indicate the guest has crossed that boundary. The +// transition timestamp is the marker time, not now, so the Initializing +// duration reflects actual guest boot time rather than the wall clock when +// hydration happened to observe the markers. +// +// Called from both the in-memory hydrate path (so the Instance returned to +// callers reflects the new phase immediately) and the persist path (so the +// updated phase is written to disk). Idempotent. +func advancePhaseIfRunning(stored *StoredMetadata) { + if stored.Phases.Current != phasetracking.PhaseInitializing { + return + } + phase, transitionAt := runningPhaseFromMarkers(stored) + if phase != phasetracking.PhaseRunning { + return + } + stored.Phases.Record(phasetracking.PhaseRunning, transitionAt) +} + // hydrateBootMarkersFromLogs fills missing boot markers from serial logs. // Returns true when at least one missing marker was found and populated. func (m *manager) hydrateBootMarkersFromLogs(ctx context.Context, stored *StoredMetadata) bool { @@ -157,6 +202,7 @@ func (m *manager) hydrateBootMarkersFromLogs(ctx context.Context, stored *Stored hydrated = true } if hydrated { + advancePhaseIfRunning(stored) m.clearBootMarkerRescan(stored.Id) } else { m.deferBootMarkerRescan(stored.Id) @@ -510,6 +556,8 @@ func (m *manager) persistBootMarkers(ctx context.Context, id string) { return } + advancePhaseIfRunning(&meta.StoredMetadata) + if err := m.saveMetadata(meta); err != nil { log.WarnContext(ctx, "failed to persist boot markers", "instance_id", id, "error", err) } else { diff --git a/lib/instances/query_test.go b/lib/instances/query_test.go index c382df8e..852f1bc4 100644 --- a/lib/instances/query_test.go +++ b/lib/instances/query_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/paths" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -169,6 +170,172 @@ func TestDeriveRunningState(t *testing.T) { } } +func TestRunningPhaseFromMarkers(t *testing.T) { + t.Parallel() + + program := time.Date(2026, 5, 11, 12, 0, 0, 0, time.UTC) + agent := program.Add(2 * time.Second) + agentBeforeProgram := program.Add(-1 * time.Second) + + tests := []struct { + name string + stored StoredMetadata + wantPhase phasetracking.Phase + wantTransAt time.Time + wantHasTrans bool + }{ + { + name: "no markers → initializing", + stored: StoredMetadata{}, + wantPhase: phasetracking.PhaseInitializing, + }, + { + name: "skip-agent + program only → running at program time", + stored: StoredMetadata{ + ProgramStartedAt: &program, + SkipGuestAgent: true, + }, + wantPhase: phasetracking.PhaseRunning, + wantTransAt: program, + wantHasTrans: true, + }, + { + name: "program without agent → still initializing", + stored: StoredMetadata{ + ProgramStartedAt: &program, + }, + wantPhase: phasetracking.PhaseInitializing, + }, + { + name: "both markers, agent later → running at agent time", + stored: StoredMetadata{ + ProgramStartedAt: &program, + GuestAgentReadyAt: &agent, + }, + wantPhase: phasetracking.PhaseRunning, + wantTransAt: agent, + wantHasTrans: true, + }, + { + name: "both markers, program later → running at program time", + stored: StoredMetadata{ + ProgramStartedAt: &program, + GuestAgentReadyAt: &agentBeforeProgram, + }, + wantPhase: phasetracking.PhaseRunning, + wantTransAt: program, + wantHasTrans: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPhase, gotAt := runningPhaseFromMarkers(&tt.stored) + assert.Equal(t, tt.wantPhase, gotPhase) + if tt.wantHasTrans { + assert.True(t, gotAt.Equal(tt.wantTransAt), "transition time = %v, want %v", gotAt, tt.wantTransAt) + } else { + assert.True(t, gotAt.IsZero(), "expected zero transition time, got %v", gotAt) + } + }) + } +} + +func TestAdvancePhaseIfRunning(t *testing.T) { + t.Parallel() + + bootStart := time.Date(2026, 5, 11, 12, 0, 0, 0, time.UTC) + program := bootStart.Add(500 * time.Millisecond) + agent := bootStart.Add(3 * time.Second) + + t.Run("initializing with no markers stays initializing", func(t *testing.T) { + stored := StoredMetadata{} + stored.Phases.Record(phasetracking.PhaseInitializing, bootStart) + + advancePhaseIfRunning(&stored) + assert.Equal(t, phasetracking.PhaseInitializing, stored.Phases.Current) + }) + + t.Run("initializing with complete markers advances using marker time", func(t *testing.T) { + stored := StoredMetadata{ + ProgramStartedAt: &program, + GuestAgentReadyAt: &agent, + } + stored.Phases.Record(phasetracking.PhaseInitializing, bootStart) + + advancePhaseIfRunning(&stored) + + assert.Equal(t, phasetracking.PhaseRunning, stored.Phases.Current) + assert.True(t, stored.Phases.Since.Equal(agent), "Since should be marker time, got %v", stored.Phases.Since) + // Initializing duration should reflect bootStart→agent, not now. + wantMs := agent.Sub(bootStart).Milliseconds() + assert.Equal(t, wantMs, stored.Phases.Cumulative[phasetracking.PhaseInitializing]) + }) + + t.Run("idempotent: already running stays running", func(t *testing.T) { + stored := StoredMetadata{ + ProgramStartedAt: &program, + GuestAgentReadyAt: &agent, + } + stored.Phases.Record(phasetracking.PhaseRunning, agent) + runningSince := stored.Phases.Since + + advancePhaseIfRunning(&stored) + + assert.Equal(t, phasetracking.PhaseRunning, stored.Phases.Current) + assert.True(t, stored.Phases.Since.Equal(runningSince), "Since should not move on idempotent call") + }) + + t.Run("non-initializing current phase is left alone", func(t *testing.T) { + // A Standby instance whose markers still indicate Running must not be + // flipped back to Running — phase transitions are driven by lifecycle + // orchestration, not by stale markers. + stored := StoredMetadata{ + ProgramStartedAt: &program, + GuestAgentReadyAt: &agent, + } + stored.Phases.Record(phasetracking.PhaseStandby, bootStart.Add(10*time.Second)) + + advancePhaseIfRunning(&stored) + assert.Equal(t, phasetracking.PhaseStandby, stored.Phases.Current) + }) +} + +func TestHydrateBootMarkersFromLogs_AdvancesPhaseOnRunningTransition(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + m := &manager{ + paths: paths.New(tmpDir), + } + + bootStart := time.Date(2026, 5, 11, 12, 0, 0, 0, time.UTC) + m.now = func() time.Time { return bootStart.Add(5 * time.Second) } + + meta := &StoredMetadata{ + Id: "phase-advance-test", + SkipGuestAgent: false, + StartedAt: &bootStart, + } + meta.Phases.Record(phasetracking.PhaseInitializing, bootStart) + + logPath := m.paths.InstanceAppLog(meta.Id) + require.NoError(t, os.MkdirAll(filepath.Dir(logPath), 0o755)) + require.NoError(t, os.WriteFile(logPath, []byte( + "HYPEMAN-AGENT-READY ts=2026-05-11T12:00:02Z\n"+ + "HYPEMAN-PROGRAM-START ts=2026-05-11T12:00:01Z mode=exec\n", + ), 0o644)) + + hydrated := m.hydrateBootMarkersFromLogs(t.Context(), meta) + require.True(t, hydrated) + require.NotNil(t, meta.ProgramStartedAt) + require.NotNil(t, meta.GuestAgentReadyAt) + + assert.Equal(t, phasetracking.PhaseRunning, meta.Phases.Current, "phase should advance to running") + // Initializing duration = bootStart → max(program, agent) = 2s. + assert.Equal(t, int64(2_000), meta.Phases.Cumulative[phasetracking.PhaseInitializing]) +} + func TestHydrateBootMarkersFromLogs_RescanThrottle(t *testing.T) { t.Parallel() diff --git a/lib/instances/restore.go b/lib/instances/restore.go index 12148b1a..d848ee48 100644 --- a/lib/instances/restore.go +++ b/lib/instances/restore.go @@ -12,7 +12,6 @@ import ( "github.com/kernel/hypeman/lib/guest" "github.com/kernel/hypeman/lib/hypervisor" - "github.com/kernel/hypeman/lib/instances/phasetracking" "github.com/kernel/hypeman/lib/logger" "github.com/kernel/hypeman/lib/network" snapshotstore "github.com/kernel/hypeman/lib/snapshot" @@ -307,8 +306,12 @@ func (m *manager) restoreInstance( // 9. Persist runtime metadata updates without resetting StartedAt. // Restore resumes an existing boot; preserving StartedAt keeps marker - // hydration scoped to the original boot timeline. - stored.Phases.Record(phasetracking.PhaseRunning, time.Now().UTC()) + // hydration scoped to the original boot timeline. The boot markers from + // the prior boot are preserved across standby, so in the common case the + // guest is back in Running immediately; if the instance was standbyed + // before markers ever hydrated we resume in Initializing. + resumePhase, _ := runningPhaseFromMarkers(stored) + stored.Phases.Record(resumePhase, time.Now().UTC()) meta = &metadata{StoredMetadata: *stored} if err := m.saveMetadata(meta); err != nil { // VM is running but metadata failed diff --git a/lib/instances/start.go b/lib/instances/start.go index a3fcd569..181aaa8d 100644 --- a/lib/instances/start.go +++ b/lib/instances/start.go @@ -210,8 +210,9 @@ func (m *manager) startInstance( // Success - release cleanup stack (prevent cleanup) cu.Release() - // 7. Update metadata (set PID, StartedAt) - stored.Phases.Record(phasetracking.PhaseRunning, time.Now().UTC()) + // 7. Update metadata (set PID, StartedAt). Boot markers were cleared at + // the top of this function, so we are in Initializing until they hydrate. + stored.Phases.Record(phasetracking.PhaseInitializing, time.Now().UTC()) meta = &metadata{StoredMetadata: *stored} if err := m.saveMetadata(meta); err != nil { // VM is running but metadata failed - log but don't fail From a29fd645fa9b5b8511496f18f522fe114a902a42 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 11 May 2026 21:49:50 +0000 Subject: [PATCH 7/8] phasetracking: drop unused PhasePaused/PhaseShutdown constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They were never recorded — internal Paused/Shutdown substates happen inside non-yielding orchestration calls and are intentionally not tracked (already documented in the package doc). --- lib/instances/phasetracking/phasetracking.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/instances/phasetracking/phasetracking.go b/lib/instances/phasetracking/phasetracking.go index 10261cf7..53f007d9 100644 --- a/lib/instances/phasetracking/phasetracking.go +++ b/lib/instances/phasetracking/phasetracking.go @@ -32,8 +32,6 @@ const ( PhaseCreated Phase = "created" PhaseInitializing Phase = "initializing" PhaseRunning Phase = "running" - PhasePaused Phase = "paused" - PhaseShutdown Phase = "shutdown" PhaseStandby Phase = "standby" ) From 3a95db61281c6a539de3d361156c66ae9b6b0829 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 11 May 2026 22:12:25 +0000 Subject: [PATCH 8/8] phasetracking: clamp marker-derived transition time to Phases.Since MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a restore from early-standby (instance standbyed before boot markers ever hydrated), Phases.Since is set at restore time. The markers parsed afterwards can carry timestamps from the pre-standby boot session, predating Since by the entire standby interval. Without the clamp, Record would silently skip the negative-elapsed accrual but still move Since backwards — and every subsequent transition would then over-count Running. Since this field feeds billing, clamp forward so Since is monotonic. Adds a regression test covering the early-standby restore path. --- lib/instances/query.go | 9 +++++++++ lib/instances/query_test.go | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lib/instances/query.go b/lib/instances/query.go index 42662a0d..2be21939 100644 --- a/lib/instances/query.go +++ b/lib/instances/query.go @@ -261,6 +261,15 @@ func advancePhaseIfRunning(stored *StoredMetadata) { if phase != phasetracking.PhaseRunning { return } + // Clamp transitionAt forward to Phases.Since. After a restore-from- + // early-standby the markers we just parsed can carry timestamps from + // the pre-standby boot session, which predate Phases.Since (set at + // restore time). Letting Since move backwards would over-count Running + // on the next transition by the entire standby interval — billing- + // critical, since the field feeds duration accounting. + if transitionAt.Before(stored.Phases.Since) { + transitionAt = stored.Phases.Since + } stored.Phases.Record(phasetracking.PhaseRunning, transitionAt) } diff --git a/lib/instances/query_test.go b/lib/instances/query_test.go index 0da91cfa..6bc8745b 100644 --- a/lib/instances/query_test.go +++ b/lib/instances/query_test.go @@ -300,6 +300,29 @@ func TestAdvancePhaseIfRunning(t *testing.T) { advancePhaseIfRunning(&stored) assert.Equal(t, phasetracking.PhaseStandby, stored.Phases.Current) }) + + t.Run("marker time before Since is clamped forward", func(t *testing.T) { + // Restore-from-early-standby: the instance was standbyed mid-boot + // before markers ever hydrated. Phases.Since is set at restore time. + // When the markers eventually parse, they may carry pre-standby + // timestamps (older than restore). Letting Since walk backwards would + // over-count Running on the next transition by the full standby + // interval — billing-critical. + restoreTime := bootStart.Add(1 * time.Hour) + stored := StoredMetadata{ + ProgramStartedAt: &program, // 500ms after bootStart — predates restore + GuestAgentReadyAt: &agent, // 3s after bootStart — also predates restore + } + stored.Phases.Record(phasetracking.PhaseInitializing, restoreTime) + + advancePhaseIfRunning(&stored) + + assert.Equal(t, phasetracking.PhaseRunning, stored.Phases.Current) + assert.True(t, stored.Phases.Since.Equal(restoreTime), + "Since must not move backwards; got %v, want %v", stored.Phases.Since, restoreTime) + // No Initializing duration is credited — elapsed at the clamp is zero. + assert.Zero(t, stored.Phases.Cumulative[phasetracking.PhaseInitializing]) + }) } func TestHydrateBootMarkersFromLogs_AdvancesPhaseOnRunningTransition(t *testing.T) {