S3: commit GET status only after the first read succeeds (#10930)

streamFromVolumeServers wrote the 200/206 status from filer metadata
before any byte had been fetched from a volume server, so a missing or
corrupted needle surfaced as a broken 200 body and the request metrics
recorded a success. Defer the status commit to the first body write: a
failed first read now returns a clean 500 before headers, while the
wire timing of successful responses is unchanged since net/http buffers
the status line until body bytes arrive anyway.
This commit is contained in:
Chris Lu authored and GitHub committed 2026-08-24 16:14:52 -07:00
1 parent d8545997c2
commit d9d5fab35b
2 files changed
+100 -23

No files matched your search

+49 -1
View File
@@ -166,7 +166,7 @@ func TestS3CachedReadFallsBackToRemote(t *testing.T) {
assert.Equal(t, int64(4), client.gotSize)
})
t.Run("a local-only object keeps the existing error", func(t *testing.T) {
t.Run("a local-only object gets a clean 500, not a broken 200 body", func(t *testing.T) {
client := &fakeStreamRemoteClient{data: content}
remote_storage.RemoteStorageClientMakers["faketest"] = &fakeStreamRemoteMaker{client: client}
s3a := newLocalReadFallbackServer(t, startStreamThroughFiler(t, "faketest-nofallback", nil))
@@ -180,6 +180,8 @@ func TestS3CachedReadFallsBackToRemote(t *testing.T) {
err := s3a.streamFromVolumeServers(w, r, local, "", "mybucket", "dir/obj.bin", "")
require.Error(t, err)
assert.Equal(t, http.StatusInternalServerError, w.Code, "the status must not be committed before the first read succeeds")
assert.Contains(t, w.Body.String(), "InternalError")
assert.Nil(t, client.gotLoc, "an object that is not remote-mounted has no remote to fall back to")
})
@@ -201,6 +203,52 @@ func TestS3CachedReadFallsBackToRemote(t *testing.T) {
})
}
// the deferred status commit must be invisible on the success path: a readable
// local object still streams with the same status and headers as before
func TestS3LocalReadCommitsStatusOnFirstWrite(t *testing.T) {
content := []byte("0123456789")
newLocalServer := func(t *testing.T, name string) *S3ApiServer {
s3a := newLocalReadFallbackServer(t, startStreamThroughFiler(t, name, nil))
cache := chunk_cache.NewChunkCacheInMemory(16)
cache.SetChunk("1,0123456789ab", content)
s3a.readerCache = filer.NewReaderCache(8, cache, s3a.filerClient.GetLookupFileIdFunction(), s3a.filerClient)
return s3a
}
localEntry := func() *filer_pb.Entry {
local := cachedEntry(content)
local.RemoteEntry = nil
return local
}
t.Run("readable local object streams a 200", func(t *testing.T) {
s3a := newLocalServer(t, "faketest-localok")
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/mybucket/dir/obj.bin", nil)
err := s3a.streamFromVolumeServers(w, r, localEntry(), "", "mybucket", "dir/obj.bin", "")
require.NoError(t, err)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, content, w.Body.Bytes())
assert.Equal(t, "10", w.Header().Get("Content-Length"))
})
t.Run("readable range streams a 206 with range headers", func(t *testing.T) {
s3a := newLocalServer(t, "faketest-localrange")
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/mybucket/dir/obj.bin", nil)
r.Header.Set("Range", "bytes=2-5")
err := s3a.streamFromVolumeServers(w, r, localEntry(), "", "mybucket", "dir/obj.bin", "")
require.NoError(t, err)
assert.Equal(t, http.StatusPartialContent, w.Code)
assert.Equal(t, content[2:6], w.Body.Bytes())
assert.Equal(t, "bytes 2-5/10", w.Header().Get("Content-Range"))
assert.Equal(t, "4", w.Header().Get("Content-Length"))
})
}
// a multi-chunk object whose later chunk's volume server is unreachable
type truncatedReaderAt struct {
data []byte