Skip to content

Skip blob retention refresh on a fallback restore - #4321

Merged
ss1909 merged 2 commits into
mainfrom
a-1777-align-fallback-restore-expiry-refresh
Sep 3, 2026
Merged

Skip blob retention refresh on a fallback restore#4321
ss1909 merged 2 commits into
mainfrom
a-1777-align-fallback-restore-expiry-refresh

Conversation

@ss1909

@ss1909 ss1909 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Description

The DynamoDB cache-entry metadata already skips its TTL bump on a fallback restore (a request whose exact key doesn't exist, served by an older, looser-matching entry instead) -- unless entry_result.fallback_used? in the backend. The reasoning: a fallback hit means the entry was saved under a shorter key sequence, and refreshing it would reset the clock on an entry the caller didn't explicitly target.

The two blob stores didn't follow the same rule. NscStore.Download ran nsc artifact extend --ensure_minimum 72h on every download, and S3Blob.Download self-CopyObject'd to reset LastModified (the field S3 lifecycle rules key off) on every download -- both unconditionally, regardless of exact-vs-fallback. restore.go already reads retrieveResp.Fallback right where it calls into the blob store; it just never got passed down, since Blob.Download's signature had no fallback parameter.

Alternative considered: none -- this mirrors an already-decided policy (metadata already does this), so the fix is just applying the same rule to the two places that didn't.

Context

https://linear.app/buildkite/issue/A-1777/align-fallback-restore-expiry-refresh-for-cache-metadata-and-blobs

Changes

  • internal/cache/store/blob.go -- Blob.Download gains a fallback bool parameter.
  • internal/cache/store/nsc.go -- skips the TTL-extend call on a fallback download.
  • internal/cache/store/s3.go -- skips the self-CopyObject refresh on a fallback download. Extracted the copy into refreshObjectExpiry, parameterized on a new objectCopier interface mirroring the existing objectDownloader/downloadWithRetry seam in the same file, so the refresh (and the fallback gate) is directly unit-testable.
  • internal/cache/store/file.go -- LocalFileBlob.Download accepts and ignores the parameter (no retention concept for local files; still required for interface conformance).
  • internal/cache/restore.go -- threads retrieveResp.Fallback into the one production Download call site.
  • Test coverage: TestNscStore_SkipsRefreshOnFallbackDownload, TestRefreshObjectExpiry's fallback case, plus existing call sites updated to preserve exact-match behavior.

Testing

  • Tests have run locally (with go test ./...)
  • Code is formatted (with go tool gofumpt -extra -w .)

Disclosures / Credits

Claude Code implemented this change end-to-end (TDD: failing test first, then the fix), under my direction and review, including a couple of follow-up simplification passes after I flagged unnecessary indirection.

NscStore and S3Blob refreshed a blob's retention (nsc artifact extend, S3
self-CopyObject to reset LastModified) on every download regardless of
whether the restore was an exact match or a fallback -- even though the
backend already skips the equivalent DynamoDB metadata TTL bump on a
fallback match, since a fallback hit means the entry was saved under a
shorter key sequence and refreshing it would reset the clock on a blob the
caller didn't explicitly target.

Add a fallback parameter to Blob.Download, threaded from
retrieveResp.Fallback (already read by restore.go, just never passed down).
Gate both stores' refresh on it. LocalFileBlob accepts and ignores the
parameter (no retention concept for local files) -- it's still required in
the signature since Go interface satisfaction needs an exact method match.

Extracted the S3 self-copy into a free function parameterized on a new
objectCopier interface, mirroring the existing objectDownloader/
downloadWithRetry pattern in the same file, so the refresh decision and the
copy itself are both directly unit-testable.
@ss1909
ss1909 requested review from a team as code owners September 3, 2026 01:16
@ss1909 ss1909 added the bug label Sep 3, 2026

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

So I think the PR makes sense - but I have a design qualm. Blob.Download taking a fallback bool doesn't make sense. Its muddling unrelated concepts.

Could we have the restore flow decide whether to call something like RefreshRetention after a successful download? I think that keeps the responsibility clearer and avoids teaching each blob store what a fallback means.

Blob.Download taking a fallback bool muddled two unrelated concerns:
downloading bytes, and deciding whether to refresh a blob's retention
afterward. It also forced LocalFileBlob to accept a parameter it had no use
for -- a store with no retention concept at all.

Revert Download to its original signature. Add a RetentionRefresher
interface (RefreshRetention(ctx, key)), implemented by NscStore and S3Blob
only -- optional, since not every store has a retention concept, rather
than forcing every implementer to satisfy it. restore.go now decides
whether to call it via a type assertion, gated on !retrieveResp.Fallback,
right after digest verification succeeds (a small correctness improvement
over the old placement inside Download: retention no longer gets refreshed
on a blob that turns out to fail digest verification).

Test coverage moves with it: each store's refresh method is tested
directly, a guard test proves Download no longer touches retention, and a
new maybeRefreshRetention helper in restore.go (with its own focused test)
owns the fallback-gating decision that used to be duplicated inside each
store.
@ss1909

ss1909 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

thanks @jamiemonserrate , that makes sense especially since LocalFileBlob had not need for the fallback arg.

I've made the following changes:

  1. abstracted it away in a RetentionRefresher interface
  2. RetentionRefresher is now implemented only by NscStore/S3Blob
  3. restore.go type-asserts for it and decides whether to call it after a successful download
  4. LocalFileBlob doesn't have to know anything about this anymore

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

LGTM 🚀

@ss1909
ss1909 merged commit fa77aa3 into main Sep 3, 2026
5 checks passed
@ss1909
ss1909 deleted the a-1777-align-fallback-restore-expiry-refresh branch September 3, 2026 04:29
@bk-docsbot

Copy link
Copy Markdown

🤖 Docs Draft Bot: I've created a draft documentation PR based on this change:

📝 Docs PR: https://github.com/buildkite/docs-private/pull/2153

Please review the draft and make any necessary adjustments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants