Repository navigation
Conversation
On a cache miss the proxy stored the whole artifact before sending any of it, so clients with an idle timeout gave up on large artifacts: git-lfs after 30 seconds, pacman after 10. The caller that starts a shared fetch now gets the bytes as they arrive while they are stored; requests joining the fetch, HEAD and range requests still wait for the stored copy. The client's copy is checked against the upstream's digest as it is read, and a mismatch aborts the response before it completes. A client going away leaves the copy to storage running. A storage error leaves the client's copy running: the client gets the artifact, and the failure is logged at error level and counted in proxy_artifacts_served_uncached_total. A block-mode scanner has to see the whole artifact first, so with one configured for the ecosystem a miss is handled as before.
andrew
left a comment
There was a problem hiding this comment.
copyToBoth writes each chunk to storeW and then to clientW in the same loop. clientW is an io.Pipe, so Write blocks until the HTTP client has read the chunk, and the upstream read and the store copy run at the client's speed. Every coalesced waiter and every instance waiting on the fetch lease is held back by that one client. The upstream body is also read through artifactClient, whose timeout is artifact_timeout (5 minutes by default). Take a 500 MB artifact sent to a client reading at 1 MB/s: the download is cut at 300 seconds, nothing is cached, and the waiters get the error. On main the same download runs at upstream speed and caches. Please decouple the client so it can't stall the store copy, for example by dropping it once it falls a bounded buffer behind, or by serving it from what has already been written. Please also add a test with a slow-reading client and a second request waiting.
Main now has #506, which conflicts with this branch in runSharedFetch. When storeArtifact returns errUnrecorded and the caller was handed the stream, the unrecorded block adds a hold for this caller and calls serveUnrecorded. The streaming goroutine closes that reader straight away, which costs the storage read the handoff return is meant to avoid. If the handoff return goes first instead, the caller's hold is never released and the file is never deleted. When handedOver(), the caller's refs.Add(1) and serveUnrecorded should be skipped and only the waiters' placeholder kept. Please also add a test in unrecorded_test.go that goes through a streamed miss.
The file-level comment at the top of stream_miss.go describes what the proxy used to do. It should say what streaming does and why: clients with idle timeouts. The storeWhileServing doc comment can be cut to two points. The client copy is checked before its last bytes, so a mismatch aborts the response. A storage failure leaves the client copy running.
# Conflicts: # internal/handler/handler.go
The upstream read fed the client through a pipe, so it ran at the client's speed. The client is now fed from a bounded buffer and, once it falls behind, continues from the stored copy where it left off. A caller handed the stream takes no hold on an unrecorded object.
Closes #596.
On a cache miss the proxy stored the whole artifact before sending any of it. Clients with an idle timeout gave up on large artifacts and retried until the detached fetch had finished.
The caller that starts the shared fetch now gets the bytes as they arrive, while they are stored (
storeWhileServingininternal/handler/stream_miss.go):artifact_timeout.cache_artifacts: falsechecks it, so a mismatch aborts the response before it completes and nothing is cached.proxy_artifacts_served_uncached_total.Content-Length. Once stored, the artifact is served with its size.Unchanged:
HEADrequests and requests with aRangeheader. The server marks only GETs withoutRange(markRequestContext).scanner.Group.Blocks).The shared fetch runs on its own goroutine (
runSharedFetchStreaming). The caller returns as soon as the fetch offers a stream, and waiters and the fetch lease still see the fetch end only once the artifact is stored.Tests (
internal/handler/stream_miss_test.go) go through the Git LFS handler with an upstream that sends the first half of an object and waits:With streaming disabled, the six streaming tests fail. The block-mode scanner test and the slow-storage test check behaviour that doesn't depend on it: the first fails when
storeFetchedignores block-mode scanners, the second when the store runs on the request's context.TestUnrecordedStreamedMiss(unrecorded_test.go) streams an unrecorded refetch: the caller handed the stream takes no hold on the object, the waiter still gets it, and it is deleted afterwards, also when the client fell behind.The streaming tests passed 10 times in a row with
-race.I also ran git-lfs 3.5.1 in a container against Hugging Face through the proxy, with the upstream throttled to about 2 MB/s, pulling a 125 MB object (
openai-community/gpt2,64-8bits.tflite):context canceled). The third attempt got the object from the cache once the detached download had finished.The pull succeeded both times only because the download fit within git-lfs's retries.