Skip to content

Fastly chunked-config GC: reclaim orphaned chunk entries on re-push (last-writer-wins)#314

Open
aram356 wants to merge 27 commits into
mainfrom
spec/fastly-chunk-gc
Open

Fastly chunked-config GC: reclaim orphaned chunk entries on re-push (last-writer-wins)#314
aram356 wants to merge 27 commits into
mainfrom
spec/fastly-chunk-gc

Conversation

@aram356

@aram356 aram356 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fastly config push chunked storage was upsert-only, so re-pushing changed app config leaked the previous generation of chunk entries: chunk keys are content-addressed by the envelope SHA, so a config change rewrites every chunk key and orphans the old set. This affected both the cloud path (remote Config Store) and the local path (fastly.toml [local_server.config_stores.<name>.contents]).

This PR adds best-effort chunk garbage collection on re-push — reclaiming the prior generation the moment a new pointer supersedes it — plus the design spec and TDD implementation plan.

Concurrency: last-writer-wins. Concurrent cloud pushes are supported (the last root-pointer write wins on the value). A push reclaims prior chunks only while a post-commit read-back confirms it is still the last writer of that root; otherwise it yields and deletes nothing, so a superseded push never removes the winner's live chunks. Best-effort, not transactional (Fastly has no compare-and-delete) — the residual is documented, bounded to one store's chunk data, and surfaces as a read-time integrity error rather than wrong data. See the spec's "Concurrency model: last-writer-wins".

Changes

Crate / File Change
docs/superpowers/specs/2026-07-07-fastly-chunk-gc.md Design spec (5 review rounds)
docs/superpowers/plans/2026-07-07-fastly-chunk-gc.md Task-by-task TDD implementation plan
crates/edgezero-adapter-fastly/src/chunked_config.rs prior_chunk_keys(root_key, raw) — Value-first, v1-validated, prefix-scoped extraction of a prior pointer's chunk keys + unit tests
crates/edgezero-adapter-fastly/src/cli.rs GC helpers (FastlyConfigGcPlan, expand_root, orphan_chunk_keys, reject_reserved_root_keys, local_contents_table); local prune in the same fastly.toml rewrite + best-effort dry-run counts; cloud post-commit sweep with last-writer-wins read-back guard + delete_config_store_entry (--key --auto-yes, never --all) + offline dry-run intent; reserved-key rejection at the adapter boundary; ~30 new tests
crates/edgezero-adapter-fastly/Cargo.toml, Cargo.lock handlebars dev-dependency (renders the cloud fake-fastly test shim)

Behavior details:

  • Reserved-infix keys (.__edgezero_chunks.) are a hard error at the adapter boundary — they'd collide with the chunk namespace.
  • Suspicious/absent prior pointers and failed deletes degrade to warnings; the push still succeeds.
  • --dry-run stays offline (cloud reports GC intent without a count; local reports an exact count, and classifies malformed prior state as unknown: could not read prior state rather than 0).
  • Non-goal: reclaiming pre-existing leaks from before this feature — deferred to a future config gc.

Closes

Closes #313

Test plan

  • cargo test -p edgezero-adapter-fastly --features cli — 112 passed
  • cargo test --workspace --all-targets — all pass
  • cargo clippy --workspace --all-targets --all-features -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • cargo check --workspace --all-targets --features "fastly cloudflare spin" — clean
  • cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin — clean
  • TDD throughout (failing test → implement → green), per-task commits

Coverage highlights: prior-pointer validation (valid/direct/garbage/wrong-kind/bad-version/foreign-prefix); reserved-key rejection (local + cloud); local prune / shrink-to-direct / suspicious-pointer-skip / sibling-chunk preservation; local dry-run counts (exact / identical-repush-zero / non-table-unknown / suspicious-unknown); cloud deletes-prior-keeps-new / read-back concurrency skip / identical-repush-no-deletes / no-prior / delete-failure-warns / prior-read-failure-warns / shrink-to-direct / delete argv asserts --key + --auto-yes and never --all.

Checklist

  • Changes follow CLAUDE.md conventions
  • No Tokio deps added to core or adapter crates
  • Types imported from edgezero_core (n/a — adapter-internal code)
  • New code has tests
  • No secrets or credentials committed

@aram356 aram356 added the documentation Improvements or additions to documentation label Jul 8, 2026
…te+scope prior_chunk_keys, offline cloud dry-run, local root inference, warning semantics, invert stale no-GC test
aram356 added 3 commits July 7, 2026 22:06
…ence unsound); Value-first prior_chunk_keys so invalid pointer-kind warns; drop 'atomic' overclaim; define local dry-run degrade semantics; state cloud GC runs only after full commit
Value-first prior_chunk_keys (pointer-kind-but-malformed warns), thread
logical roots into write_fastly_local_config_store via roots: &[&str]
(no infix inference, since --key is free-form), best-effort local
dry-run counts, post-commit-only cloud sweep. Task-by-task with TDD
steps for subagent-driven-development.
…entical re-push counts 0 (was over-counting); enumerate all 10 writer call sites + roots args; forbid --all on delete; reword failed-delete warnings as informational (inert, future config gc); note sequential-spawn latency + approximate line numbers
@aram356 aram356 removed the documentation Improvements or additions to documentation label Jul 8, 2026
aram356 added 2 commits July 8, 2026 07:41
…t root read-back guard (invariant 5); build keep-sets from per-root expand_root instead of prefix-scanning flattened entries; make reserved-infix --key rejection mandatory at the Fastly adapter boundary + flip the infix test to expect rejection; add local suspicious-pointer real-push test and cloud concurrency-guard test
… (drop 'race-safe' overclaim, add Concurrency model section + plan precondition gate); correct cost note for the post-commit read-back describe; add dry-run suspicious-pointer test
@aram356 aram356 marked this pull request as draft July 8, 2026 15:42
aram356 added 6 commits July 8, 2026 08:48
Concurrent cloud pushes are SUPPORTED: root pointer is upsert so the last
write wins on the value. GC obeys LWW via the post-commit read-back guard
— a push reclaims prior chunks only while it is still the last writer of
the root, else it yields (never deletes the winner's live chunks).
Removes the single-writer assumption and the blocking 'do not implement'
gate; keeps the honest best-effort residual-window note. No code.
…inter test (seed real chunk keys, assert they survive); expand_root errors on empty instead of silent default; reserved-key error wording drops --key assumption
…k_keys, reject_reserved_root_keys, FastlyConfigGcPlan) + unit tests

Wired into push paths in the following commits; transient dead-code warnings until then.
write_fastly_local_config_store takes exact per-root keep-sets (gc_roots)
and prunes orphaned chunk keys in the same in-memory rewrite; suspicious
prior pointers warn and delete nothing. push_config_entries_local rejects
reserved keys, threads per-root expansion, and reports best-effort orphan
counts in dry-run. Inverts the stale no-GC test; adds shrink-to-direct,
suspicious-pointer, reserved-key, and dry-run count/identical/unknown tests.
…riter-wins)

push_config_entries rejects reserved keys, reads each root's prior value
before commit, and after the commit sweeps orphaned chunks guarded by a
post-commit root read-back (deletes only while still the last writer;
yields otherwise). Adds delete_config_store_entry (--key --auto-yes, never
--all) and an offline dry-run GC-intent line. Failed deletes and
suspicious/absent priors degrade to warnings; the push still succeeds.
Adds a command-aware fake fastly harness and 7 cloud GC tests.
@aram356 aram356 changed the title Spec: Fastly chunked-config GC to reclaim orphaned chunk entries on re-push Fastly chunked-config GC to reclaim orphaned chunk entries on re-push Jul 8, 2026
aram356 added 2 commits July 8, 2026 13:56
…astly via handlebars

Moves FastlyConfigGcPlan to the struct group with alphabetical fields;
renames single-char closure idents; replaces bare arithmetic with
saturating_add; fixes map_err/shadow/assert-on-result-state/absolute-path
lints; relocates GC helper unit tests after the test-module structs. Adds
handlebars dev-dependency and rewrites the cloud fake-fastly test shim to
render its shell script from a handlebars template.
@aram356 aram356 added the rust Pull requests that update rust code label Jul 8, 2026
@aram356 aram356 changed the title Fastly chunked-config GC to reclaim orphaned chunk entries on re-push Fastly chunked-config GC: reclaim orphaned chunk entries on re-push Jul 8, 2026
- local: GC of a chunked root leaves a chunked sibling's chunks intact
  (prefix-scoping vs shared string prefix app_config / app_config_staging)
- cloud: identical-bytes re-push deletes nothing (read-back returns our
  own value, so the assertion is non-vacuous)
- cloud: prior-read failure warns and deletes nothing (extends the fake
  fastly with a describe_hard_error mode)
@aram356 aram356 self-assigned this Jul 8, 2026
@aram356 aram356 removed the rust Pull requests that update rust code label Jul 8, 2026
…t delete argv + cloud shrink-to-direct

- local dry-run: distinguish absent (0) from present-but-non-table
  ("unknown: could not read prior state") via local_contents_table, so
  --local --dry-run no longer reports 0 orphans for state the real writer
  would reject; + non-table-contents test
- cloud: assert every delete argv passes --key + --auto-yes and NEVER
  --all (blast radius); fake now logs the full delete argv
- cloud: add the shrink-to-direct test (prior chunked -> new direct
  deletes all prior chunks, root upserted not deleted)
@aram356 aram356 changed the title Fastly chunked-config GC: reclaim orphaned chunk entries on re-push Fastly chunked-config GC: reclaim orphaned chunk entries on re-push (last-writer-wins) Jul 9, 2026
@aram356 aram356 marked this pull request as ready for review July 9, 2026 06:32

@prk-Jr prk-Jr 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.

🔧 Summary

The local reclamation path and delete-set validation are well implemented, but the cloud sweep needs a safe reclamation boundary before prior chunks can be deleted. Immediate deletion can break readers under eventual propagation or a concurrent push.

📝 CI Status

  • fmt: PASS
  • clippy: PASS
  • tests: PASS

Comment thread crates/edgezero-adapter-fastly/src/cli.rs Outdated
Comment thread crates/edgezero-adapter-fastly/src/chunked_config.rs

@ChristianPavilonis ChristianPavilonis 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.

Summary

Reviewed the Fastly chunk-GC implementation and supporting tests/specification. Approving with two follow-up findings included as inline comments.

Comment thread crates/edgezero-adapter-fastly/src/cli.rs Outdated
Comment thread crates/edgezero-adapter-fastly/src/cli.rs
aram356 added 3 commits July 12, 2026 14:26
…uplicate root keys (P2)

P1 (security): fetch/read error paths interpolated the raw `describe`
stdout — i.e. the stored config envelope, which may hold credentials —
into errors the CLI logs verbatim. The GC prior-read runs on EVERY cloud
push, so this exposed the previous config on any schema drift. Now report
only size + top-level field-name shape via redact_describe_response; adds
sentinel-secret tests for both the read and push paths.

P2 (correctness): a duplicate logical root in one batch built two GC plans
against the same prior; the earlier plan reclaimed the chunks the LAST
tuple had just installed, leaving the final pointer dangling. Reject
duplicate root keys in both push paths, before any expansion or I/O.
…nder eventual consistency)

Fastly Config Store is eventually consistent: after the control plane accepts
pointer N, POPs may still serve pointer N-1 and need N-1's chunks. The
post-commit read-back only observes the CONTROL PLANE, so eager deletion could
strip chunks from a POP mid-propagation -- breaking readers on every re-push,
not merely under concurrent writers. (Reported on PR #314.)

Cloud GC is now DEFERRED:
- a push never deletes what it just superseded; it RECORDS that generation in
  a per-root pending record at <root>.__edgezero_gc.pending
- a later push reclaims a recorded generation only once it has aged past a
  grace window (EDGEZERO_FASTLY_GC_GRACE_SECS, default 24h) AND nothing live
  references it (neither the new generation nor the just-superseded one)
- the record stores (sha, count, superseded_at) per generation, not raw keys:
  keys are derivable, and the raw list would blow the 8 000-char entry limit
- it is a LIST, so pushing faster than the grace window cannot drop an
  un-reclaimed generation and leak it untracked
- failed deletes keep the generation pending so a later push retries
- the LWW read-back guard still gates all GC state changes

The LOCAL path keeps eager pruning: fastly.toml is a single file Viceroy reads
at startup -- no propagation window, no POPs.

Extracts reclaim_and_record; adds pending-record round-trip and
derive_chunk_keys tests, plus reclaim-after-grace / retain-within-grace /
defer-instead-of-delete cloud tests.
…and duplicate-root invariants

Records the design correction from the PR #314 review: eager cloud deletion is
unsafe under Fastly's eventual consistency (the read-back only sees the control
plane, not POP propagation), so the superseded generation is recorded and
reclaimed by a later push after a grace window. Documents the pending-record
shape (compact sha+count, list-not-slot), the 24h configurable grace, and the
two further invariants (never echo config payloads; reject duplicate roots).
@aram356

aram356 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks both — all three findings were real and are now addressed.

@prk-Jr — eager cloud deletion (blocking) ✅

You're right, and it's worse than a concurrency edge case: the read-back only observes the control plane, so it says nothing about POP propagation. Eager deletion could strip chunks from a POP still serving the previous pointer on every re-push, not just under concurrent writers. Knowing what to delete was never the problem — knowing when it's safe is, and no control-plane read answers that.

Cloud GC is now deferred. A push never deletes what it just superseded; it records that generation, and a later push reclaims it once it has aged out:

  • pending record at <root>.__edgezero_gc.pending (reserved namespace, rejected in logical keys)
  • reclaim only when: age >= GRACE and the keys are referenced by neither the new generation nor the just-superseded one (N-1 is retained unconditionally)
  • grace window EDGEZERO_FASTLY_GC_GRACE_SECS, default 24h (Fastly documents no propagation bound, so the default is deliberately generous)
  • record stores (sha, count, superseded_at) — keys are derivable, and a raw key list would blow the record's own 8 000-char limit
  • it's a list, not a slot: pushing faster than the grace window must not drop an un-reclaimed generation and leak it untracked
  • failed deletes keep the generation pending so a later push retries

The local path keeps eager pruning — fastly.toml is a single file Viceroy reads at startup, so there's no propagation window and no POP.

New tests: defers_prior_generation_instead_of_deleting, reclaims_pending_generation_after_grace, retains_pending_generation_within_grace.

@ChristianPavilonis — P1, prior config in push logs ✅

Confirmed and fixed. redact_describe_response now reports only the response's size and top-level field-name shape — never a value — at all four sites (the two fetch_remote_config_store_entry paths and the two read_config_entry paths). Added sentinel-secret tests asserting the token never appears in the returned error (read path) or in the push's status lines (the GC prior-read path you flagged).

@ChristianPavilonis — P2, duplicate roots in one batch ✅

Confirmed exactly as described: two plans against the same prior, and A's plan reclaims the chunks B just installed. Duplicate logical root keys are now a hard error in both push paths, before any expansion or I/O — rejecting rather than coalescing, since a duplicated key is a caller bug and picking a winner would hide it. Regression test uses your scenario (prior B, batch [A, B]) and asserts the root still resolves.


Spec updated with the design correction and both invariants. Green: 123 fastly tests, clippy --workspace --all-targets --all-features -D warnings, fmt --check, full workspace tests, and the Spin wasm32 check.

aram356 added 3 commits July 13, 2026 08:37
… too

The stdout fix was half a fix. The describe and update --stdin paths carry the
stored config value, so a Fastly failure that quotes the payload back in stderr
put credentials into logs via the FAILURE branch — the same exposure, different
door. Redact stderr on those three value-bearing sites (not-found classification
still inspects it internally; only the user-facing string is suppressed).
delete/create/list stderr carry no value and are left verbatim.

Adds sentinel-secret tests for the stderr path on both the read and push paths,
complementing the existing successful-but-drifted stdout tests.
…the metadata sidecar

The pending-record protocol was unsound and unfixable in place (PR #314 review):
Fastly has no CAS, so a failed sidecar write / failed read-back / concurrent
push PERMANENTLY lost a generation (and the 'will retry' warnings were false);
and the record itself overflowed the 8000-char entry limit at ~71 generations,
after which every later generation was silently lost.

Verified against the live Fastly API and rebuilt with NO metadata at all --
the store IS the state:

- `config-store-entry list --json` returns item_key + created_at per entry
- a generation is superseded exactly when the NEXT one is written, so
  superseded_at(G) = created_at(successor(G)), read straight off the listing
- reclaim G iff it is unreferenced by the live pointer AND
  now - superseded_at(G) >= grace (EDGEZERO_FASTLY_GC_GRACE_SECS, default 24h)
- the generation this push just superseded is ALWAYS protected, so POPs that
  have not yet seen the new pointer keep their chunks

IMPORTANT: the root entry's own updated_at is NOT usable -- Fastly does not bump
it on `update --upsert`. Verified live: a root last 'updated' 2026-07-07 pointed
at chunks created 2026-07-13. Using it as the supersession clock would have
reclaimed the previous generation immediately.

This dissolves four review findings structurally rather than patching them:
no record to be malformed or to overflow, no lost updates, and deletes now
target the store's ACTUAL keys (never keys re-derived from a content address,
which could retarget onto keys the pointer never referenced).

chunk_key_generation validates the key shape (hex sha + numeric index) so a
hand-edited or foreign key can never become a delete target.
…an superseded

Resolves the contradiction the review flagged: the spec declared deferred
reclamation while its algorithm/cloud sections still specified eager deletion,
and the plan was entirely eager-delete based. There is now ONE authoritative
contract.

The spec records both rejected designs (eager delete; metadata sidecar) and why,
so the mistakes are not repeated, and documents the live-API findings that drove
the final design -- including that the root entry's updated_at is NOT bumped by
`update --upsert` and therefore cannot be used as a supersession clock.

The plan is retained as a historical build guide with a prominent SUPERSEDED
banner pointing at the spec.
@aram356

aram356 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

All six findings addressed. The first four were fatal to the metadata design, not patchable — so I removed the metadata entirely.

The sidecar is gone. The store IS the state.

I probed the live Fastly API and found two things:

(a) config-store-entry list --json returns item_key + created_at per item.
(b) The root entry's updated_at is NOT bumped by update --upsert. Observed on a real store: a root whose updated_at read 2026-07-07 was pointing at chunks created 2026-07-13. Had I used it as the supersession clock (my first instinct), it would have concluded the current generation had been stable for six days and reclaimed the previous one immediately — the exact unsafe outcome. Worth recording.

Chunk entries' own created_at is accurate. And a generation is superseded exactly when the next one is written, so:

superseded_at(G) = created_at(successor(G))     # straight off the listing

Post-commit, per root: read-back (LWW) → list → group this root's actual keys into generations → protected = keys(N) ∪ keys(N-1) → delete generation G iff keys(G) ∩ protected = ∅ and now − superseded_at(G) ≥ GRACE (default 24h, EDGEZERO_FASTLY_GC_GRACE_SECS).

This dissolves four findings structurally rather than patching them:

Finding Resolution
malformed pending record / unbounded count no record exists. chunk_key_generation validates <root>.__edgezero_chunks.<hex-sha>.<index>, so a foreign/hand-edited key is never even grouped
compaction retargets deletes we delete the listing's actual keys; nothing is re-derived from a SHA
lost updates (no CAS) no metadata to lose. Reclamation is stateless and idempotent — a failed run just recomputes next push. The "will retry" warnings are now true
8 000-byte overflow at ~71 generations no sidecar to overflow

The just-superseded generation is unconditionally protected — there's a test asserting it survives even with GRACE=0. Bonus: this reclaims the pre-existing backlog too, which the sidecar never could.

Remaining two

Payload redaction — completed. You were right that I'd only closed half the door: describe/update --stdin carry the config value, so a Fastly failure quoting it back in stderr leaked via the failure branch. redact_stderr now covers those three value-bearing sites (not-found classification still inspects stderr internally). Sentinel-secret tests on both stdout-drift and stderr-failure, for both the read and push paths.

Docs — reconciled. There is now one authoritative contract: the spec describes only the shipped design and records both rejected designs (eager delete; sidecar) with the reasons. The plan carries a prominent SUPERSEDED banner pointing at the spec.


Gates: 121 fastly tests, workspace tests, fmt --check, clippy --workspace --all-targets --all-features -D warnings, all-adapter feature compilation, Spin wasm32 — all green.

Please re-review the cloud path in full; it is a different design, not a patched one.

aram356 added 2 commits July 14, 2026 11:08
…oked `config gc`

A cloud push no longer deletes anything. Safe automatic reclamation is not
achievable with Fastly's primitives, and three designs failed review proving it:

1. eager delete -> unsafe (eventual consistency: POPs may still serve the
   previous pointer, and the read-back only sees the control plane)
2. metadata sidecar -> unsound (no CAS => lost updates; 8000-char overflow)
3. store-derived clock -> unsound (chunk created_at is NOT pointer-supersession
   time: a chunked -> direct -> direct transition leaves the old generation with
   no 'successor' at all, so it ages from its own creation and gets deleted while
   POPs still serve it)

The missing fact -- 'the pointer that referenced these chunks has stopped being
served everywhere' -- is one Fastly does not record (updated_at is not bumped by
an upsert; verified live) and that we cannot safely synthesise. Only the OPERATOR
knows their deploy history.

So reclamation becomes an explicit `config gc`:
- new Adapter::gc_config_entries (default: unimplemented) + Fastly impl
- one `config-store-entry list`; roots are classified via their pointers, so
  live chunks are known exactly
- deletes only unreferenced chunk entries older than the operator's --older-than
- FAILS CLOSED: an unclassifiable root or an unreadable created_at aborts with
  nothing deleted
- a dry-run prints every key and age it would delete, so the operator's
  assertion is reviewable

The local path keeps eager pruning (one file, no POPs) and is unchanged.
…ossible

Resolves the spec's self-contradiction (it claimed pre-existing leaks were both
reclaimed and never reclaimed) and makes it the single authoritative contract.

Records all three rejected automatic designs (eager delete; metadata sidecar;
store-derived clock) with the concrete counterexample that killed each, and the
live-API findings behind them -- notably that Fastly does not bump updated_at on
an upsert, and that chunk created_at is not a pointer-supersession time.

States the conclusion plainly: the fact needed to delete safely is one the
platform does not record and we cannot synthesise, so the operator supplies it
via `config gc --older-than`. Plan banner updated to match.
@aram356

aram356 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

You were right a fourth time, and this one settles it. I've stopped trying to make cloud reclamation automatic.

Your P1 #1 kills the premise

superseded_at(G) = created_at(successor(G)) assumes chunk creation tracks pointer transitions. It doesn't. Your counterexample is exact: chunked A → direct B → direct C — during C, A is the only chunk generation listed, so it has no successor, ages from its own creation, and gets deleted although B superseded it seconds ago. Partial pushes and chunk/pointer write gaps break it identically. My tests passed because they encoded the flawed premise.

The conclusion I should have reached three rounds ago

To delete safely you must know the pointer that referenced these chunks has stopped being served everywhere. Fastly:

  • doesn't record itupdated_at is not bumped by update --upsert (verified live);
  • offers no CAS to record it ourselves (your sidecar lost-update finding);
  • and created_at is not a proxy for it (this round).

Even embedding a written_at in the pointer fails: under a concurrent push you protect the predecessor you read, not the one you replaced — and if that push reverted to old content, its chunks carry old created_at, so no freshness filter saves them (your P1 #2).

Safe automatic cloud reclamation is not achievable with Fastly's primitives. So:

What shipped

  • A cloud push now deletes nothing. Cloud accretes orphans exactly as today — no regression, and the only safe automatic behavior.
  • Reclamation is an explicit config gc (Adapter::gc_config_entries + Fastly impl). One list; roots are classified via their pointers so the live set is known exactly; deletes only unreferenced chunks older than the operator's --older-than. --older-than is the assertion the machine cannot make and the operator can.
  • It fails CLOSED (your P1 Added unit tests for CLI #3): an unclassifiable root or an unreadable created_at aborts with nothing deleted. A dry-run prints every key + age it would delete, so the assertion is reviewable.
  • Canonical key validation (your P2 Implemented axum adapter #4): only <root>.__edgezero_chunks.<hex-sha>.<index> is ever a candidate.
  • Local eager pruning is unchanged and remains correct (one file, no POPs).

New config gc tests: never-deletes-live-chunks · reclaims-older-than-threshold · retains-younger · dry-run-lists-but-deletes-nothing · fails-closed-on-unreadable-timestamp · fails-closed-on-unclassifiable-root. The vacuous listing-failure test you flagged is gone.

Spec/plan (your P2 #6): the self-contradiction is fixed; the spec is now the single authoritative contract and records all three rejected designs with the counterexample that killed each, so a fourth isn't attempted.

Gates: 115 fastly tests, workspace tests, fmt, clippy -D warnings, feature check, Spin wasm32 — all green.

aram356 added 2 commits July 14, 2026 15:05
# Conflicts:
#	crates/edgezero-adapter-fastly/src/cli.rs
Adds `edgezero config gc --adapter <name> [--older-than <dur>] [--yes]`,
dispatching to Adapter::gc_config_entries. Runs IN-BAND in the bundled binary
(and app-demo) -- unlike push/diff it needs no typed app-config, only the
store's own contents -- via manifest-only resolution (no <app>.toml required).

Safe by default: without --yes it is a dry-run that lists every key + age it
would delete. --older-than accepts 7d/24h/90m/30s or bare seconds (default 7d)
and is the operator's safety assertion; parse_duration_secs rejects garbage so a
destructive command never guesses its threshold.

Verified against the live store: correctly identifies the 3 live chunks from the
root pointer and, at --older-than 1h, would reclaim exactly the 33 orphaned
entries (11 dead generations) while deleting nothing without --yes.
@aram356

aram356 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Update: merged main, resolved all review threads, and wired config gc end-to-end

Conflicts with main: resolved (the only conflict was edition-2021 → 2024 use-ordering vs my gc_roots param). Merge is clean; all gates green.

All four review threads resolved with what changed:

  • Eager cloud GC (unsafe) → automatic cloud deletion removed entirely; a push now deletes nothing.
  • P1 log leak → stdout and stderr redaction on every value-bearing path, sentinel-tested.
  • P2 duplicate roots → hard-rejected in both push paths, before any I/O; regression test.
  • Blast-radius (positive) → canonical key validation retained + config gc fails closed.

config gc is now a real, wired command (bundled edgezero + app-demo):

edgezero config gc --adapter fastly [--older-than 7d] [--yes]

Safe by default (dry-run without --yes); --older-than is the operator's safety assertion; fails closed on unclassifiable/unreadable state.

Verified against a live Fastly store (read-only; no item_value printed): it correctly identified the 3 live chunks from the root pointer, and at --older-than 1h listed exactly the 33 orphaned entries (11 dead generations) it would delete — deleting nothing without --yes.

Gates: 1195 workspace tests, fmt --check, clippy --workspace --all-targets --all-features -D warnings, feature check, Spin wasm32 — all green.

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.

Fastly chunked-config GC: reclaim orphaned chunk entries on re-push

3 participants