From a05973bf2b011d9ebf38f91ca458c9444f9cae66 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sun, 5 Apr 2026 00:21:05 -0300 Subject: [PATCH] hecate: exclude dry-run history from shutdown budget estimation --- internal/cluster/orchestrator.go | 2 ++ internal/state/store.go | 3 ++- internal/state/store_test.go | 39 ++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/internal/cluster/orchestrator.go b/internal/cluster/orchestrator.go index 2e3c47f..0c3d134 100644 --- a/internal/cluster/orchestrator.go +++ b/internal/cluster/orchestrator.go @@ -98,6 +98,7 @@ func (o *Orchestrator) Startup(ctx context.Context, opts StartupOptions) (err er ID: fmt.Sprintf("startup-%d", time.Now().UnixNano()), Action: "startup", Reason: opts.Reason, + DryRun: o.runner.DryRun, StartedAt: time.Now().UTC(), } defer o.finalizeRecord(&record, &err) @@ -343,6 +344,7 @@ func (o *Orchestrator) Shutdown(ctx context.Context, opts ShutdownOptions) (err ID: fmt.Sprintf("shutdown-%d", time.Now().UnixNano()), Action: "shutdown", Reason: opts.Reason, + DryRun: o.runner.DryRun, StartedAt: time.Now().UTC(), } defer o.finalizeRecord(&record, &err) diff --git a/internal/state/store.go b/internal/state/store.go index bb9feb3..e24f449 100644 --- a/internal/state/store.go +++ b/internal/state/store.go @@ -19,6 +19,7 @@ type RunRecord struct { ID string `json:"id"` Action string `json:"action"` Reason string `json:"reason,omitempty"` + DryRun bool `json:"dry_run,omitempty"` StartedAt time.Time `json:"started_at"` EndedAt time.Time `json:"ended_at"` DurationSeconds int `json:"duration_seconds"` @@ -207,7 +208,7 @@ func (s *Store) shutdownP95(defaultSeconds int, minSamples int, reasonPrefixes [ return false } for _, r := range records { - if r.Action == "shutdown" && r.Success && r.DurationSeconds > 0 && matchesPrefix(r.Reason) { + if r.Action == "shutdown" && !r.DryRun && r.Success && r.DurationSeconds > 0 && matchesPrefix(r.Reason) { d = append(d, r.DurationSeconds) } } diff --git a/internal/state/store_test.go b/internal/state/store_test.go index 838054e..fdc552b 100644 --- a/internal/state/store_test.go +++ b/internal/state/store_test.go @@ -160,3 +160,42 @@ func TestShutdownP95ByReasonPrefixFiltersSamples(t *testing.T) { t.Fatalf("expected filtered p95 of 220, got %d", got) } } + +func TestShutdownP95IgnoresDryRunSamples(t *testing.T) { + p := filepath.Join(t.TempDir(), "runs.json") + now := time.Now().UTC() + records := []RunRecord{ + { + ID: "shutdown-dryrun", + Action: "shutdown", + Reason: "ups-threshold target=Pyrphoros", + DryRun: true, + StartedAt: now, + EndedAt: now, + DurationSeconds: 5, + Success: true, + }, + { + ID: "shutdown-real", + Action: "shutdown", + Reason: "ups-threshold target=Pyrphoros", + DryRun: false, + StartedAt: now, + EndedAt: now, + DurationSeconds: 210, + Success: true, + }, + } + b, err := json.Marshal(records) + if err != nil { + t.Fatalf("marshal records: %v", err) + } + if err := os.WriteFile(p, b, 0o640); err != nil { + t.Fatalf("write records: %v", err) + } + + got := New(p).ShutdownP95ByReasonPrefix(420, 1, []string{"ups-"}) + if got != 210 { + t.Fatalf("expected dry-run sample to be ignored and real value 210 used, got %d", got) + } +}