Repository navigation
feat(lfs): write LFS objects under their own @lfs prefix - #274
Merged
Merged
Conversation
PaxMachinaOne
left a comment
Collaborator
There was a problem hiding this comment.
🤖 PaxMachina automated review (worker-code)
The new layout works normally, but generation changes can place live references under the wrong incarnation.
cmd/proxy/lfs_http.go:769— Multipart uploads select and persist the generation-specific key only at upload initialization. A topic can be deleted and recreated before completion, after whichhandleHTTPUploadCompletepublishes the envelope to the new incarnation while retaining the old generation’s key. This defeats incarnation isolation and could make a live object eligible for future old-generation pruning. Revalidate/guard the generation at completion and cleanly abort if it changed; the streaming and intercepted-produce paths have a smaller version of the same lookup-to-publish race.
PaxMachinaOne
left a comment
Collaborator
There was a problem hiding this comment.
🤖 PaxMachina automated review (worker-code)
Generation selection has a delete/recreate race that can place objects under the wrong incarnation.
- Correctness:
cmd/proxy/lfs_rewrite.go:137reads the generation once, then uploads later at line 180 without revalidating it or coupling it atomically to the Kafka produce. If the topic is deleted and recreated between those operations, the record can be produced to generation N+1 while its object is stored under@lfs/<topic>@N/. The HTTP and multipart paths have the same TOCTOU window atcmd/proxy/lfs_http.go:295and line 769; multipart uploads can make it especially long. This defeats the promised per-incarnation separation and could make future generation-based LFS pruning unsafe.
PaxMachinaOne
left a comment
Collaborator
There was a problem hiding this comment.
🤖 PaxMachina automated review (worker-code)
The new prefix handling is sound, but multi-topic generation validation leaves a correctness race.
cmd/proxy/lfs_rewrite.go:243— Generation rechecks run sequentially over every topic, each with its own timeout. An early topic can therefore be deleted and recreated while later checks consume up to 3 seconds each, after which its record is produced with an object from the stale generation. For multi-topic requests, the documented “short gap” can grow to roughlytopic count × metadata timeout. Recheck generations as one consistent etcd read/transaction, or at least concurrently immediately before dispatch.
PaxMachinaOne
approved these changes
Oct 31, 2026
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.
New LFS objects now go under their own prefix instead of inside the topic's segment folder.
Layout:
<ns>/<topic>/lfs/yyyy/mm/dd/obj-<uuid>, inside generation 0's topic folder.<ns>/@lfs/<topic>/yyyy/mm/dd/obj-<uuid>, and<ns>/@lfs/<topic>@<N>/...after the Nth delete. This uses the same storage name as the segments (metadata.TopicStorageName).Why
@lfs: Kafka topic names only allow[a-zA-Z0-9._-], so@lfscan never be a topic prefix.@already marks generations in storage names.Generation: the proxy reads it from etcd when etcd is configured. Without etcd it uses 0. If the lookup fails, the upload fails instead of writing to the wrong generation.
Old objects: they are not moved. The envelope stores the full key, and the proxy download, the Go resolver and the SDKs read
env.keyas is. The download key check accepts both/lfs/and/@lfs/.Prune is unchanged. Its segment allowlist skips both old and new LFS keys. LFS pruning is not part of this PR.
Tests:
@lfskeysMetadata lookup timeout
The topic generation lookup runs with a timeout, 3000 ms by default, set with
KAFSCALE_LFS_PROXY_METADATA_TIMEOUT_MS. When etcd is configured but not reachable,/lfs/produceand/lfs/uploadsreturn 503metadata_unavailable, and a Kafka produce with LFS records returns an error. Generations are looked up once per topic before any upload, so a failed lookup uploads nothing. The download key check accepts only<ns>/@lfs/...and the old<ns>/<topic>/lfs/...shape.Generation recheck before produce
After the upload and right before the produce, the proxy looks up the topic generation again, with the same bounded lookup. This covers the Kafka rewrite path, HTTP
/lfs/produce, and multipart complete. If the generation changed or the lookup fails, the proxy deletes the uploaded objects, reports objects it cannot delete as orphans, does not produce, and returns the metadata error: 503metadata_unavailableover HTTP, and the retriable produce error on Kafka.Remaining window: a delete and recreate that happens between this second lookup and the produce is not caught. The gap is one etcd read plus encoding the request, so it is small but not zero. Closing it fully would need the produce itself to be fenced on the generation.