Skip to content

feat(wfctl): rotate-and-prune --prune-first flag for at-quota safety (v0.27.2)#592

Merged
intel352 merged 3 commits into
mainfrom
feat/rotate-and-prune-first-flag
May 10, 2026
Merged

feat(wfctl): rotate-and-prune --prune-first flag for at-quota safety (v0.27.2)#592
intel352 merged 3 commits into
mainfrom
feat/rotate-and-prune-first-flag

Conversation

@intel352

Copy link
Copy Markdown
Contributor

Summary

Adds --prune-first flag to wfctl infra rotate-and-prune (default true)
that flips step ordering: pre-prune orphans BEFORE rotation, then rotate,
then defensive post-prune sweep. Closes the at-quota chicken-and-egg.

Why

Before this PR, rotate-and-prune ran rotate then prune in fixed order. When
the cloud account is at quota (DO Spaces enforces 200 keys per project),
Step 1's Create call fails with HTTP 403: key quota exceeded before
Step 2 (prune) can free quota. Operators had to hand-prune via the cloud
console first — exactly the manual workflow rotate-and-prune was built to
eliminate.

What changes

  • cmd/wfctl/infra_rotate_and_prune.go:

    • New --prune-first boolean flag, default true (per ADR 0023).
    • When --prune-first=true, runs runPreRotationPrune (NEW helper) BEFORE
      Step 1. The pre-prune deletes resources where Name != --name AND
      Name does not match --preserve-names regex.
    • Step 2 (post-rotation prune) re-labelled as "defensive sweep" when
      --prune-first=true — should be a no-op if Step 0 was complete, but
      covers replacement of canonical-name's old value.
    • When --prune-first=false, the v0.27.1 ordering is preserved exactly.
    • Pre-prune defensively re-checks WFCTL_CONFIRM_PRUNE=1. A failed
      pre-prune delete aborts BEFORE rotation (no minting on a half-cleaned
      account).
    • Pre-flight EnumerateAll probe + ErrProviderMethodUnimplemented
      handling unchanged.
  • cmd/wfctl/infra_rotate_and_prune_test.go:

    • 4 new tests (see Test plan).
    • 1 new fake quotaCappedFakeProvider that mutates outputs as
      DeleteResource calls land — closer to real cloud-API semantics.
  • decisions/0023-rotate-and-prune-prune-first-default.md:

    • New ADR documenting the default flip + four rejected alternatives.

Filter shape

Pre-prune uses a NAME-based filter (canonical --name + --preserve-names
skipped). The TIME + ACCESS_KEY filter that runInfraPrune uses is not
applicable here because no rotation result exists yet at this point in the
flow. Post-prune retains its TIME + ACCESS_KEY filter unchanged.

Test plan

  • GOWORK=off go build ./... — clean
  • GOWORK=off go test ./cmd/wfctl/... -count=1 -run RotateAndPrune -v
    8/8 pass (4 existing + 4 new)
  • GOWORK=off go test ./... — full suite passes
  • golangci-lint run ./cmd/wfctl/... — 0 issues

New test functions:

  • TestRotateAndPrune_PruneFirst_HappyPath_AtQuota — orphans + canonical-old
    loaded; default --prune-first=true deletes orphans pre-rotation, rotates
    cleanly, post-prune cleans canonical-old. Asserts Step 0 + defensive sweep
    banners in output.
  • TestRotateAndPrune_PruneFirst_DefaultTrue — regression sentinel for the
    ADR 0023 default flip. Orphan with created_at newer than rotation
    timestamp is deleted by default (would survive legacy ordering).
  • TestRotateAndPrune_PruneFirst_False_LegacyOrder — opt-out path. Same
    newer-than-cutoff orphan must NOT be deleted under --prune-first=false.
    Output must NOT mention Step 0 / defensive sweep.
  • TestRotateAndPrune_PruneFirst_PreservesCanonicalName — canonical
    --name + --preserve-names regex match must both be skipped during
    pre-prune. Asserted via "Pre-rotation dry-run: 1 orphan" output line.

Behavior change call-out

Default --prune-first=true changes observable behavior on the
default invocation: orphan keys (Name != canonical, not in preserve regex)
created AFTER the rotation timestamp are now deleted, where previously
they survived the post-rotation time filter. This is intentional per
ADR 0023 — orphans are orphans regardless of when they were created.
Callers needing legacy semantics use --prune-first=false.

DO NOT

  • Do NOT merge this PR — orchestrator handles admin-merge + tag v0.27.2.

ADR

decisions/0023-rotate-and-prune-prune-first-default.md

References ADR 0012 (provider credential rotation), ADR 0017 (prune two-key
opt-in), ADR 0020 (storage filter sidecar metadata).

intel352 added 3 commits May 9, 2026 21:22
…t-quota safety

Adds --prune-first to runInfraRotateAndPrune that flips step ordering: pre-prune
orphans (name != canonical, not in --preserve-names) BEFORE rotation, then
rotate, then defensive post-prune sweep. Default is true; --prune-first=false
preserves the v0.27.1 ordering for callers that need byte-exact legacy behavior.

Closes the at-quota chicken-and-egg: when DO Spaces hits its 200-key cap,
Step 1 (rotate = mint new key) used to fail with HTTP 403 "key quota exceeded"
before Step 2 (prune) ever got a chance to free quota. Operators had to
hand-prune via the cloud console first — exactly the workflow rotate-and-prune
was built to eliminate.

Pre-prune uses a NAME-based filter (skips the canonical --name + the
--preserve-names regex). The TIME + ACCESS_KEY filter that runInfraPrune uses
is not applicable here because no rotation result exists yet at this point.
A failed pre-prune delete aborts BEFORE rotation: better to leave a partially-
pruned account than to mint a new key on a half-cleaned one.

The pre-flight EnumerateAll probe + ErrProviderMethodUnimplemented handling
(v0.27.1) is unchanged. WFCTL_CONFIRM_PRUNE=1 + --confirm gate the entire
flow including the pre-prune step (the helper defensively re-checks).
Four new tests covering the --prune-first flag:

- TestRotateAndPrune_PruneFirst_HappyPath_AtQuota: orphans + canonical-old
  loaded; --prune-first default true deletes orphans pre-rotation, rotates
  cleanly, post-prune cleans canonical-old. Asserts Step 0 + defensive sweep
  banners in output.

- TestRotateAndPrune_PruneFirst_DefaultTrue: regression sentinel for the
  ADR 0023 default flip. With an orphan whose created_at is NEWER than the
  rotation timestamp, default behavior must delete it (would survive the
  post-rotation time filter under legacy ordering).

- TestRotateAndPrune_PruneFirst_False_LegacyOrder: opt-out path. Same
  newer-than-cutoff orphan must NOT be deleted under --prune-first=false.
  Output must NOT mention Step 0 / defensive sweep.

- TestRotateAndPrune_PruneFirst_PreservesCanonicalName: canonical --name +
  --preserve-names regex match must both be skipped during pre-prune.
  Asserted via the "Pre-rotation dry-run: 1 orphan" output line.

Adds quotaCappedFakeProvider helper that mutates its EnumerateAll output as
DeleteResource calls land — closer to the real cloud-API semantics than the
existing static-list fakeProviderEnumerableDriver.
Documents the default flip from prune-after to prune-first. Cites the
at-quota chicken-and-egg as motivation: the very condition operators need
the cleanup tool for makes the legacy step-1-then-step-2 order unusable.

References workspace memory feedback_proper_fixes_over_workarounds — the
default IS the strict behavior; the legacy ordering is opt-out via
--prune-first=false for byte-exact regressions.

Lists four rejected alternatives (opt-in flag, auto-detect quota, split
into two commands, enum-valued flag) with rationale for each.
Copilot AI review requested due to automatic review settings May 10, 2026 01:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates wfctl infra rotate-and-prune to be safer when cloud accounts are at credential quota by adding a --prune-first flag (default true) that performs a pre-rotation orphan prune, then rotates, then performs a post-rotation defensive prune sweep. It also adds an ADR documenting the default flip and new unit tests covering the new ordering.

Changes:

  • Add --prune-first (default true) and a new runPreRotationPrune helper to prune orphan credentials before attempting rotation.
  • Adjust user-visible step/banner output to reflect Step 0/1/2 when pre-prune is enabled and label the post-prune as a “defensive sweep”.
  • Add tests for default behavior, legacy opt-out behavior, quota safety behavior, and preserve/canonical-name protection; add ADR 0023 documenting the rationale.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
decisions/0023-rotate-and-prune-prune-first-default.md Adds ADR documenting why --prune-first defaults to true and what behavior changes.
cmd/wfctl/infra_rotate_and_prune.go Implements --prune-first flow and runPreRotationPrune helper; updates step ordering and output.
cmd/wfctl/infra_rotate_and_prune_test.go Adds new tests and a new fake provider to validate pre-prune behavior and legacy ordering opt-out.

Comment on lines +291 to +297
if pruneFirst {
fmt.Fprintf(w, "Step 0 (--prune-first): pruning orphan %s resources before rotation...\n", resourceType)
if code := runPreRotationPrune(ctx, provider, resourceType, name, preserveNames, nonInteractive, w); code != 0 {
fmt.Fprintf(w, "\nrotate-and-prune: pre-rotation prune failed (code=%d). No rotation attempted; no state mutated.\n", code)
return code
}
}
Comment on lines +502 to +508
ref := interfaces.ResourceRef{Type: o.Type, Name: o.Name, ProviderID: o.ProviderID}
if delErr := provider.DeleteResource(ctx, ref); delErr != nil {
fmt.Fprintf(w, "rotate-and-prune: pre-rotation delete %s: %v\n", o.Name, delErr)
failed++
continue
}
fmt.Fprintf(w, " ✓ deleted %s\n", o.Name)
Comment on lines +228 to +238
// quotaCappedFakeProvider simulates a cloud account at the per-resource-type
// quota (e.g., DO Spaces 200-key limit). EnumerateAll returns the current
// `outputs` slice; DeleteResource shrinks `outputs` (removes the matching
// ProviderID) and records the delete in `deleted`.
//
// `createOnRotate` is the metadata the bootstrapSecrets stub will return for
// the new key. The test driver invokes the stub which appends this to
// `outputs`; if `len(outputs) >= quota` at append time the stub errors with
// quotaErr to simulate the at-quota Create failure. Tests assert that with
// --prune-first=true the orphan deletes happen FIRST, freeing room before
// the stub appends, so the rotation succeeds.
@github-actions

Copy link
Copy Markdown

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:262: parsing iteration count: invalid syntax
baseline-bench.txt:293582: parsing iteration count: invalid syntax
baseline-bench.txt:584301: parsing iteration count: invalid syntax
baseline-bench.txt:932419: parsing iteration count: invalid syntax
baseline-bench.txt:1272632: parsing iteration count: invalid syntax
baseline-bench.txt:1603341: parsing iteration count: invalid syntax
benchmark-results.txt:262: parsing iteration count: invalid syntax
benchmark-results.txt:334943: parsing iteration count: invalid syntax
benchmark-results.txt:664887: parsing iteration count: invalid syntax
benchmark-results.txt:965245: parsing iteration count: invalid syntax
benchmark-results.txt:1231757: parsing iteration count: invalid syntax
benchmark-results.txt:1565868: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ baseline-bench.txt │       benchmark-results.txt        │
                            │       sec/op       │    sec/op     vs base              │
InterpreterCreation-4              3.121m ± 215%   6.729m ± 56%       ~ (p=0.180 n=6)
ComponentLoad-4                    3.607m ±   9%   3.566m ±  1%  -1.14% (p=0.041 n=6)
ComponentExecute-4                 1.939µ ±   1%   1.916µ ±  2%       ~ (p=0.065 n=6)
PoolContention/workers-1-4         1.083µ ±   0%   1.077µ ±  3%       ~ (p=0.169 n=6)
PoolContention/workers-2-4         1.083µ ±   2%   1.086µ ±  2%       ~ (p=0.974 n=6)
PoolContention/workers-4-4         1.088µ ±   1%   1.072µ ±  2%  -1.43% (p=0.039 n=6)
PoolContention/workers-8-4         1.086µ ±   1%   1.077µ ±  2%       ~ (p=0.069 n=6)
PoolContention/workers-16-4        1.093µ ±   2%   1.075µ ±  1%  -1.60% (p=0.009 n=6)
ComponentLifecycle-4               3.576m ±   0%   3.579m ±  1%       ~ (p=0.589 n=6)
SourceValidation-4                 2.291µ ±   0%   2.290µ ±  1%       ~ (p=0.965 n=6)
RegistryConcurrent-4               812.7n ±   5%   798.7n ±  2%       ~ (p=0.394 n=6)
LoaderLoadFromString-4             3.585m ±   0%   3.586m ±  1%       ~ (p=0.589 n=6)
geomean                            17.43µ          18.46µ        +5.90%

                            │ baseline-bench.txt │        benchmark-results.txt         │
                            │        B/op        │     B/op      vs base                │
InterpreterCreation-4               2.027Mi ± 0%   2.027Mi ± 0%       ~ (p=0.310 n=6)
ComponentLoad-4                     2.180Mi ± 0%   2.180Mi ± 0%       ~ (p=0.331 n=6)
ComponentExecute-4                  1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4         1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                2.183Mi ± 0%   2.183Mi ± 0%       ~ (p=0.255 n=6)
SourceValidation-4                  1.984Ki ± 0%   1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                1.133Ki ± 0%   1.133Ki ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4              2.182Mi ± 0%   2.182Mi ± 0%       ~ (p=0.983 n=6)
geomean                             15.25Ki        15.25Ki       +0.00%
¹ all samples are equal

                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │     allocs/op      │  allocs/op   vs base                │
InterpreterCreation-4                15.68k ± 0%   15.68k ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                      18.02k ± 0%   18.02k ± 0%       ~ (p=1.000 n=6)
ComponentExecute-4                    25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4           25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                 18.07k ± 0%   18.07k ± 0%       ~ (p=1.000 n=6) ¹
SourceValidation-4                    32.00 ± 0%    32.00 ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                  2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4               18.06k ± 0%   18.06k ± 0%       ~ (p=1.000 n=6) ¹
geomean                               183.3         183.3       +0.00%
¹ all samples are equal

pkg: github.com/GoCodeAlone/workflow/middleware
                                  │ baseline-bench.txt │       benchmark-results.txt       │
                                  │       sec/op       │   sec/op     vs base              │
CircuitBreakerDetection-4                  285.2n ± 6%   285.4n ± 2%       ~ (p=1.000 n=6)
CircuitBreakerExecution_Success-4          21.48n ± 0%   21.54n ± 0%  +0.30% (p=0.002 n=6)
CircuitBreakerExecution_Failure-4          66.24n ± 1%   66.14n ± 0%  -0.15% (p=0.013 n=6)
geomean                                    74.04n        74.08n       +0.06%

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │        B/op        │    B/op     vs base                │
CircuitBreakerDetection-4                 144.0 ± 0%     144.0 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │     allocs/op      │ allocs/op   vs base                │
CircuitBreakerDetection-4                 1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │       sec/op       │    sec/op     vs base              │
JQTransform_Simple-4                     877.6n ± 25%   930.7n ± 22%       ~ (p=0.937 n=6)
JQTransform_ObjectConstruction-4         1.450µ ±  1%   1.443µ ±  2%       ~ (p=0.253 n=6)
JQTransform_ArraySelect-4                3.305µ ±  1%   3.286µ ±  0%  -0.59% (p=0.026 n=6)
JQTransform_Complex-4                    38.16µ ±  1%   37.80µ ±  0%  -0.92% (p=0.026 n=6)
JQTransform_Throughput-4                 1.774µ ±  3%   1.766µ ±  0%       ~ (p=0.104 n=6)
SSEPublishDelivery-4                     63.34n ±  0%   63.49n ±  0%       ~ (p=0.121 n=6)
geomean                                  1.619µ         1.629µ        +0.61%

                                 │ baseline-bench.txt │        benchmark-results.txt         │
                                 │        B/op        │     B/op      vs base                │
JQTransform_Simple-4                   1.273Ki ± 0%     1.273Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4       1.773Ki ± 0%     1.773Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4              2.625Ki ± 0%     2.625Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                  16.22Ki ± 0%     16.22Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4               1.984Ki ± 0%     1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │     allocs/op      │ allocs/op   vs base                │
JQTransform_Simple-4                     10.00 ± 0%     10.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4         15.00 ± 0%     15.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4                30.00 ± 0%     30.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                    324.0 ± 0%     324.0 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4                 17.00 ± 0%     17.00 ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │       sec/op       │    sec/op     vs base              │
SchemaValidation_Simple-4                   1.105µ ±  4%   1.200µ ± 27%       ~ (p=0.387 n=6)
SchemaValidation_AllFields-4                1.707µ ± 28%   1.686µ ±  2%       ~ (p=0.240 n=6)
SchemaValidation_FormatValidation-4         1.606µ ±  2%   1.610µ ±  2%       ~ (p=0.937 n=6)
SchemaValidation_ManySchemas-4              1.822µ ±  4%   1.826µ ±  3%       ~ (p=0.784 n=6)
geomean                                     1.533µ         1.562µ        +1.89%

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │        B/op        │    B/op     vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │     allocs/op      │ allocs/op   vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
                                   │ baseline-bench.txt │       benchmark-results.txt        │
                                   │       sec/op       │    sec/op     vs base              │
EventStoreAppend_InMemory-4                1.192µ ± 11%   1.171µ ± 27%       ~ (p=0.937 n=6)
EventStoreAppend_SQLite-4                  1.381m ±  2%   1.326m ±  4%  -4.04% (p=0.015 n=6)
GetTimeline_InMemory/events-10-4           13.62µ ±  7%   14.41µ ±  3%       ~ (p=0.065 n=6)
GetTimeline_InMemory/events-50-4           77.15µ ±  2%   79.70µ ± 19%       ~ (p=0.065 n=6)
GetTimeline_InMemory/events-100-4          126.0µ ± 23%   128.4µ ±  3%       ~ (p=0.065 n=6)
GetTimeline_InMemory/events-500-4          645.9µ ±  1%   654.1µ ±  0%  +1.27% (p=0.002 n=6)
GetTimeline_InMemory/events-1000-4         1.332m ±  1%   1.349m ±  1%  +1.29% (p=0.002 n=6)
GetTimeline_SQLite/events-10-4             107.7µ ±  1%   109.7µ ±  0%  +1.85% (p=0.002 n=6)
GetTimeline_SQLite/events-50-4             252.6µ ±  1%   256.0µ ±  1%  +1.31% (p=0.002 n=6)
GetTimeline_SQLite/events-100-4            430.3µ ±  1%   434.6µ ±  0%  +1.00% (p=0.002 n=6)
GetTimeline_SQLite/events-500-4            1.848m ±  0%   1.843m ±  0%  -0.30% (p=0.002 n=6)
GetTimeline_SQLite/events-1000-4           3.589m ±  1%   3.587m ±  0%       ~ (p=0.589 n=6)
geomean                                    222.3µ         224.4µ        +0.94%

                                   │ baseline-bench.txt │        benchmark-results.txt         │
                                   │        B/op        │     B/op      vs base                │
EventStoreAppend_InMemory-4                  790.5 ± 7%     787.0 ± 5%       ~ (p=0.608 n=6)
EventStoreAppend_SQLite-4                  1.986Ki ± 1%   1.986Ki ± 3%       ~ (p=0.723 n=6)
GetTimeline_InMemory/events-10-4           7.953Ki ± 0%   7.953Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4           46.62Ki ± 0%   46.62Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4          94.48Ki ± 0%   94.48Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4          472.8Ki ± 0%   472.8Ki ± 0%       ~ (p=1.000 n=6)
GetTimeline_InMemory/events-1000-4         944.3Ki ± 0%   944.3Ki ± 0%       ~ (p=0.424 n=6)
GetTimeline_SQLite/events-10-4             16.74Ki ± 0%   16.74Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4             87.14Ki ± 0%   87.14Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4            175.4Ki ± 0%   175.4Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4            846.1Ki ± 0%   846.1Ki ± 0%       ~ (p=1.000 n=6)
GetTimeline_SQLite/events-1000-4           1.639Mi ± 0%   1.639Mi ± 0%       ~ (p=0.123 n=6)
geomean                                    67.35Ki        67.33Ki       -0.04%
¹ all samples are equal

                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │     allocs/op      │  allocs/op   vs base                │
EventStoreAppend_InMemory-4                  7.000 ± 0%    7.000 ± 0%       ~ (p=1.000 n=6) ¹
EventStoreAppend_SQLite-4                    53.00 ± 0%    53.00 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-10-4             125.0 ± 0%    125.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4             653.0 ± 0%    653.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4           1.306k ± 0%   1.306k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4           6.514k ± 0%   6.514k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-1000-4          13.02k ± 0%   13.02k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-10-4               382.0 ± 0%    382.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4              1.852k ± 0%   1.852k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4             3.681k ± 0%   3.681k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4             18.54k ± 0%   18.54k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-1000-4            37.29k ± 0%   37.29k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                     1.162k        1.162k       +0.00%
¹ all samples are equal

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

@intel352 intel352 merged commit d495f74 into main May 10, 2026
24 checks passed
@intel352 intel352 deleted the feat/rotate-and-prune-first-flag branch May 10, 2026 03:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants