[blob-store 2/5] Reference-aware GC for v2/ blobs + gzip the manifest - #7
Closed
chondl wants to merge 2 commits into
Closed
[blob-store 2/5] Reference-aware GC for v2/ blobs + gzip the manifest#7chondl wants to merge 2 commits into
chondl wants to merge 2 commits into
Conversation
chondl
force-pushed
the
blob-gc
branch
2 times, most recently
from
July 10, 2026 20:27
8baafe1 to
0338012
Compare
List v2/ objects, subtract the current manifest's references and objects younger than a 48h grace window, and delete the remainder in batches. Exposed as a data-router endpoint (grace_hours and dry_run params) so a scheduler can trigger it. Legacy paths, hist/, and manifest.json are never touched.
manifest.json is refetched per client each minute and is the dominant egress cost. Store it gzipped with Content-Encoding: gzip; GCS transcoding serves it compressed to browsers (which send Accept-Encoding: gzip) and transparently decompressed otherwise, so no reader change is needed.
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on
bucket-first-serving. Two independent changes to the blob publishlayer: a reference-aware garbage collector for the versioned
v2/blobs, and agzip content-encoding for
manifest.json. One commit per change. No new tests onthis branch.
1. Reference-aware GC for
v2/blobsProblem. Copy-on-write publishing uploads a blob to a new content-addressed
key
v2/{path}.{hash12}only when its payload changes, and the manifest flips topoint at the new key. The superseded key is never referenced again but is never
deleted, so
v2/grows without bound. The PR description recommended a GCSlifecycle rule, but a blind "delete objects older than N days" rule is unsafe: a
blob that has not changed in months is still current and still referenced by the
manifest, and age alone cannot distinguish it from a superseded version.
Design.
gc_versioned_blobs()insrc/google/storage.py:manifest.json; collect its referenced versioned keys.v2/.a grace window (
GC_GRACE_HOURS = 48, a constant).bucket.delete_blobs, 100 at a time).It only ever lists and deletes under the
v2/prefix, so legacy unversionedpaths,
hist/, andmanifest.jsonare never touched. It is exposed as a GET onthe data router (
/v3/data/gc_blobs, alongside the existingupdate_curr_yearetc.) so Cloud Scheduler can trigger it, with optional
grace_hoursanddry_runquery params. It is idempotent — a second run with no intervening publish finds
nothing new to delete — and it logs a one-line summary
(
GC v2/: scanned … kept … deleted … freed … bytes).Concurrency race and why the grace window is correct. A publish uploads new
v2/objects before it writes the manifest that references them (manifest-lastatomicity). If GC ran concurrently with a publish and only subtracted the
current manifest's references, it could see a freshly uploaded object that the
about-to-be-written manifest will reference, judge it unreferenced, and delete it —
tearing the set the publish is assembling. The grace window closes this: any
object younger than 48h is kept regardless of reference state, so an in-flight
publish's new objects are always protected until well after its manifest lands.
The same window also protects objects referenced by a previous manifest that a
live client still holds in its 60s cache (plus CDN TTL) — 48h is far beyond any
realistic client/edge cache lifetime — so a client resolving an old manifest never
races a delete. GC is therefore safe to run concurrently with a publish and safe
to run on any schedule; it never needs to coordinate with the publisher.
Dry-run evidence (local rig, fake-gcs, full 2026 set).
v2/held 4,483objects, of which the current manifest referenced 3,950.
grace_hours=48, dry_run=true→ scanned 4,484, kept 4,484, deleted 0 — everyobject on the rig was created recently, so the grace window protected all of
them, including a planted unreferenced probe. This is the young-object rule in
action.
grace_hours=0, dry_run=true→ scanned 4,484, kept 3,950, would delete 534(533 accumulated superseded versions + 1 planted probe), freeing 858,001 bytes;
nothing actually deleted.
grace_hours=0, dry_run=false→ deleted 534, freed 858,001 bytes; a 25-keysample of manifest-referenced objects all survived (25/25), and the planted
probe was gone.
fake-gcs sets
time_createdto now for every object, so the grace window wasparameterized to 0 to exercise the delete path in the same run; production uses
the 48h default.
2. gzip
manifest.jsonProblem.
manifest.jsonis ~169 KB of plain JSON and is refetched by everyclient every 60s (
max-age=60) — the dominant egress line item at scale.Design.
write_manifest()now stores the manifest gzip-compressed withcontent_encoding='gzip'. GCS transcoding serves the compressed bytes to clientsthat send
Accept-Encoding: gzip(all browsers) and transparently decompressesfor those that do not, so no reader changes: the frontend's
fetch(...).json()and the backend's
download_as_bytes()both receive plain JSON unchanged.Measured. manifest plain JSON 169,138 bytes → gzipped 53,569 bytes (0.317×,
a 68% reduction) at 3,950 entries. Round-trip verified:
read_manifest()returnsan identical blob map after the gzip write.
No reader change, verified through fake-gcs on the rig.
curlof the gzippedmanifest:
Accept-Encoding: gzip→Content-Encoding: gzip, 53,569 bytes, gzip magic1f8b.Content-Encoding, 169,138 bytes, body starts{(decompressed).fake-gcs reproduces real GCS transcoding, so the rig is sufficient here.
Notes
STYLE: no code comments, smallest reviewable diff, one commit per change.
Pure-logic tests for the GC keep/delete partition can follow on a stacked
*-testsbranch if wanted, matching this stack's convention of keeping testinfrastructure off the feature branch.