s3: invalidate stale reader cache locations on chunk read failure (#10156)

* s3: invalidate stale reader cache locations on chunk read failure

* filer: share the chunk-read self-heal across reader cache and streaming paths

The reader cache retry added a third copy of the invalidate-relookup-compare-retry
dance already inlined in PrepareStreamContentWithThrottler and duplicated in
retryWithCacheInvalidation. Extract retryFetchWithFreshLocations and route all
three through it, parameterized by the refetch primitive.

* filer: drop redundant completedTimeNew store in reader cache success path

startCaching already stamps completedTimeNew unconditionally before the
fetchErr branch; the second store inside the success branch is dead.

* filer: make NewReaderCache cache invalidator an explicit parameter

The variadic ...CacheInvalidator only ever read the first element, so a caller
could pass two and silently get one. Take a single explicit argument and have
the non-S3 callers pass nil.

* filer: inject reader cache chunk fetch as a struct field

Replace the process-global readerCacheFetchChunkData test seam with a
per-instance fetchChunkDataFn field defaulted in NewReaderCache, matching how
lookupFileIdFn is already wired. Tests set the field on the cache instead of
swapping a shared global.

* filer: log the location count, not full URLs, on self-heal retry

---------

Co-authored-by: Chris Lu <chris.lu@gmail.com>
This commit is contained in:
Aleksey
2026-06-30 13:27:49 -07:00
committed by GitHub
co-authored by Chris Lu
parent 803c5a8dca
commit 424cd164e9
10 changed files with 302 additions and 115 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ func NewChunkGroup(lookupFn wdclient.LookupFileIdFunctionType, chunkCache chunk_
group := &ChunkGroup{
lookupFn: lookupFn,
sections: make(map[SectionIndex]*FileChunkSection),
readerCache: NewReaderCache(readerCacheLimit, chunkCache, lookupFn),
readerCache: NewReaderCache(readerCacheLimit, chunkCache, lookupFn, nil),
concurrentReaders: concurrentReaders,
}
+5 -5
View File
@@ -77,7 +77,7 @@ func TestReaderAt(t *testing.T) {
readerAt := &ChunkReadAt{
chunkViews: ViewFromVisibleIntervals(visibles, 0, math.MaxInt64),
fileSize: 10,
readerCache: NewReaderCache(3, &mockChunkCache{}, nil),
readerCache: NewReaderCache(3, &mockChunkCache{}, nil, nil),
readerPattern: NewReaderPattern(),
}
@@ -124,7 +124,7 @@ func TestReaderAt0(t *testing.T) {
readerAt := &ChunkReadAt{
chunkViews: ViewFromVisibleIntervals(visibles, 0, math.MaxInt64),
fileSize: 10,
readerCache: NewReaderCache(3, &mockChunkCache{}, nil),
readerCache: NewReaderCache(3, &mockChunkCache{}, nil, nil),
readerPattern: NewReaderPattern(),
}
@@ -150,7 +150,7 @@ func TestReaderAt1(t *testing.T) {
readerAt := &ChunkReadAt{
chunkViews: ViewFromVisibleIntervals(visibles, 0, math.MaxInt64),
fileSize: 20,
readerCache: NewReaderCache(3, &mockChunkCache{}, nil),
readerCache: NewReaderCache(3, &mockChunkCache{}, nil, nil),
readerPattern: NewReaderPattern(),
}
@@ -183,7 +183,7 @@ func TestReaderAtGappedChunksDoNotLeak(t *testing.T) {
readerAt := &ChunkReadAt{
chunkViews: ViewFromVisibleIntervals(visibles, 0, math.MaxInt64),
fileSize: 9,
readerCache: NewReaderCache(3, &mockChunkCache{}, nil),
readerCache: NewReaderCache(3, &mockChunkCache{}, nil, nil),
readerPattern: NewReaderPattern(),
}
@@ -195,7 +195,7 @@ func TestReaderAtSparseFileDoesNotLeak(t *testing.T) {
readerAt := &ChunkReadAt{
chunkViews: ViewFromVisibleIntervals(NewIntervalList[*VisibleInterval](), 0, math.MaxInt64),
fileSize: 3,
readerCache: NewReaderCache(3, &mockChunkCache{}, nil),
readerCache: NewReaderCache(3, &mockChunkCache{}, nil, nil),
readerPattern: NewReaderPattern(),
}
+86 -24
View File
@@ -14,9 +14,17 @@ import (
"github.com/seaweedfs/seaweedfs/weed/wdclient"
)
type CacheInvalidator interface {
InvalidateCache(fileId string)
}
type fetchChunkDataFnType func(ctx context.Context, buffer []byte, urlStrings []string, cipherKey []byte, isGzipped bool, isFullChunk bool, offset int64, fileId string) (n int, err error)
type ReaderCache struct {
chunkCache chunk_cache.ChunkCache
lookupFileIdFn wdclient.LookupFileIdFunctionType
chunkCache chunk_cache.ChunkCache
lookupFileIdFn wdclient.LookupFileIdFunctionType
cacheInvalidator CacheInvalidator
fetchChunkDataFn fetchChunkDataFnType
sync.Mutex
downloaders map[string]*SingleChunkCacher
limit int
@@ -38,12 +46,14 @@ type SingleChunkCacher struct {
done chan struct{} // signals when download is complete
}
func NewReaderCache(limit int, chunkCache chunk_cache.ChunkCache, lookupFileIdFn wdclient.LookupFileIdFunctionType) *ReaderCache {
func NewReaderCache(limit int, chunkCache chunk_cache.ChunkCache, lookupFileIdFn wdclient.LookupFileIdFunctionType, cacheInvalidator CacheInvalidator) *ReaderCache {
return &ReaderCache{
limit: limit,
chunkCache: chunkCache,
lookupFileIdFn: lookupFileIdFn,
downloaders: make(map[string]*SingleChunkCacher),
limit: limit,
chunkCache: chunkCache,
lookupFileIdFn: lookupFileIdFn,
cacheInvalidator: cacheInvalidator,
fetchChunkDataFn: util_http.RetriedFetchChunkData,
downloaders: make(map[string]*SingleChunkCacher),
}
}
@@ -97,15 +107,25 @@ func (rc *ReaderCache) MaybeCache(chunkViews *Interval[*ChunkView], count int) {
func (rc *ReaderCache) ReadChunkAt(ctx context.Context, buffer []byte, fileId string, cipherKey []byte, isGzipped bool, offset int64, chunkSize int, shouldCache bool) (int, error) {
rc.Lock()
if cacher, found := rc.downloaders[fileId]; found {
rc.Unlock()
n, err := cacher.readChunkAt(ctx, buffer, offset)
if n > 0 || err != nil {
return n, err
for {
if cacher, found := rc.downloaders[fileId]; found {
if cacher.hasCompletedError() {
delete(rc.downloaders, fileId)
rc.Unlock()
cacher.destroy()
rc.Lock()
continue
}
rc.Unlock()
n, err := cacher.readChunkAt(ctx, buffer, offset)
if n > 0 || err != nil {
return n, err
}
// If n=0 and err=nil, the cacher couldn't provide data for this offset.
// Fall through to try chunkCache.
rc.Lock()
}
// If n=0 and err=nil, the cacher couldn't provide data for this offset.
// Fall through to try chunkCache.
rc.Lock()
break
}
if shouldCache || rc.lookupFileIdFn == nil {
n, err := rc.chunkCache.ReadChunkAt(buffer, fileId, uint64(offset))
@@ -198,32 +218,74 @@ func (s *SingleChunkCacher) startCaching() {
// Lookup file ID without holding the lock
urlStrings, err := s.parent.lookupFileIdFn(context.Background(), s.chunkFileId)
if err != nil {
s.Lock()
s.err = fmt.Errorf("operation LookupFileId %s failed, err: %v", s.chunkFileId, err)
s.Unlock()
s.setError(fmt.Errorf("operation LookupFileId %s failed, err: %v", s.chunkFileId, err))
return
}
if len(urlStrings) == 0 {
s.setError(fmt.Errorf("operation LookupFileId %s failed, err: urls not found", s.chunkFileId))
return
}
// Allocate buffer and download without holding the lock
// This allows multiple downloads to proceed in parallel
data := mem.Allocate(s.chunkSize)
_, fetchErr := util_http.RetriedFetchChunkData(context.Background(), data, urlStrings, s.cipherKey, s.isGzipped, true, 0, s.chunkFileId)
data, fetchErr := s.fetchChunkData(context.Background(), urlStrings)
if fetchErr != nil {
data, fetchErr = s.retryFetchAfterCacheInvalidation(context.Background(), urlStrings, fetchErr)
}
// Now acquire lock to update state
s.Lock()
atomic.StoreInt64(&s.completedTimeNew, time.Now().UnixNano())
if fetchErr != nil {
mem.Free(data)
s.err = fetchErr
} else {
s.data = data
if s.shouldCache {
s.parent.chunkCache.SetChunk(s.chunkFileId, s.data)
}
atomic.StoreInt64(&s.completedTimeNew, time.Now().UnixNano())
}
s.Unlock()
}
func (s *SingleChunkCacher) setError(err error) {
s.Lock()
defer s.Unlock()
s.err = err
atomic.StoreInt64(&s.completedTimeNew, time.Now().UnixNano())
}
func (s *SingleChunkCacher) hasCompletedError() bool {
if atomic.LoadInt64(&s.completedTimeNew) == 0 {
return false
}
s.Lock()
defer s.Unlock()
return s.err != nil
}
func (s *SingleChunkCacher) fetchChunkData(ctx context.Context, urlStrings []string) ([]byte, error) {
// Allocate buffer and download without holding the lock.
// This allows multiple downloads to proceed in parallel.
data := mem.Allocate(s.chunkSize)
_, fetchErr := s.parent.fetchChunkDataFn(ctx, data, urlStrings, s.cipherKey, s.isGzipped, true, 0, s.chunkFileId)
if fetchErr != nil {
mem.Free(data)
return nil, fetchErr
}
return data, nil
}
func (s *SingleChunkCacher) retryFetchAfterCacheInvalidation(ctx context.Context, oldUrlStrings []string, originalErr error) ([]byte, error) {
var data []byte
err := retryFetchWithFreshLocations(ctx, s.parent.cacheInvalidator, s.parent.lookupFileIdFn, s.chunkFileId, oldUrlStrings, originalErr, func(newUrls []string) error {
var fetchErr error
data, fetchErr = s.fetchChunkData(ctx, newUrls)
return fetchErr
})
if err != nil {
return nil, err
}
return data, nil
}
func (s *SingleChunkCacher) destroy() {
// wait for all reads to finish before destroying the data
s.wg.Wait()
+154 -18
View File
@@ -16,6 +16,25 @@ type mockChunkCacheForReaderCache struct {
mu sync.Mutex
}
type mockCacheInvalidatorForReaderCache struct {
calls int32
mu sync.Mutex
fileId string
}
func (m *mockCacheInvalidatorForReaderCache) InvalidateCache(fileId string) {
atomic.AddInt32(&m.calls, 1)
m.mu.Lock()
defer m.mu.Unlock()
m.fileId = fileId
}
func (m *mockCacheInvalidatorForReaderCache) lastFileId() string {
m.mu.Lock()
defer m.mu.Unlock()
return m.fileId
}
func newMockChunkCacheForReaderCache() *mockChunkCacheForReaderCache {
return &mockChunkCacheForReaderCache{
data: make(map[string][]byte),
@@ -60,6 +79,113 @@ func (m *mockChunkCacheForReaderCache) IsInCache(fileId string, lockNeeded bool)
return ok
}
func TestReaderCacheRetryAfterCacheInvalidation(t *testing.T) {
cache := newMockChunkCacheForReaderCache()
invalidator := &mockCacheInvalidatorForReaderCache{}
fileId := "425141,ef8914e9bdbbe8cb6838191f"
staleUrl := "http://fast-volume-1/" + fileId
freshUrl := "http://fast-volume-9/" + fileId
testData := []byte("fresh chunk data after cache invalidation")
var lookupCount int32
lookupFn := func(ctx context.Context, requestedFileId string) ([]string, error) {
if requestedFileId != fileId {
return nil, fmt.Errorf("unexpected lookup file id %s", requestedFileId)
}
atomic.AddInt32(&lookupCount, 1)
if atomic.LoadInt32(&invalidator.calls) == 0 {
return []string{staleUrl}, nil
}
return []string{freshUrl}, nil
}
var fetchCount int32
fetchFn := func(ctx context.Context, buffer []byte, urlStrings []string, cipherKey []byte, isGzipped bool, isFullChunk bool, offset int64, requestedFileId string) (int, error) {
if requestedFileId != fileId {
return 0, fmt.Errorf("unexpected fetch file id %s", requestedFileId)
}
switch atomic.AddInt32(&fetchCount, 1) {
case 1:
if len(urlStrings) != 1 || urlStrings[0] != staleUrl {
return 0, fmt.Errorf("first fetch should use stale url %v", urlStrings)
}
return 0, fmt.Errorf("404 Not Found: not found")
case 2:
if len(urlStrings) != 1 || urlStrings[0] != freshUrl {
return 0, fmt.Errorf("retry fetch should use fresh url %v", urlStrings)
}
return copy(buffer, testData), nil
default:
return 0, fmt.Errorf("unexpected extra fetch with urls %v", urlStrings)
}
}
rc := NewReaderCache(10, cache, lookupFn, invalidator)
rc.fetchChunkDataFn = fetchFn
defer rc.destroy()
buffer := make([]byte, len(testData))
n, err := rc.ReadChunkAt(context.Background(), buffer, fileId, nil, false, 0, len(testData), true)
if err != nil {
t.Fatalf("expected successful retry, got %v", err)
}
if got := string(buffer[:n]); got != string(testData) {
t.Fatalf("expected %q, got %q", testData, got)
}
if got := atomic.LoadInt32(&invalidator.calls); got != 1 {
t.Fatalf("expected one cache invalidation, got %d", got)
}
if got := invalidator.lastFileId(); got != fileId {
t.Fatalf("expected invalidated file id %s, got %s", fileId, got)
}
if got := atomic.LoadInt32(&lookupCount); got != 2 {
t.Fatalf("expected lookup before fetch and after invalidation, got %d", got)
}
if got := atomic.LoadInt32(&fetchCount); got != 2 {
t.Fatalf("expected stale fetch and retry fetch, got %d", got)
}
}
func TestReaderCacheRemovesFailedDownloader(t *testing.T) {
cache := newMockChunkCacheForReaderCache()
fileId := "425141,failed"
url := "http://fast-volume-1/" + fileId
var lookupCount int32
lookupFn := func(ctx context.Context, requestedFileId string) ([]string, error) {
if requestedFileId != fileId {
return nil, fmt.Errorf("unexpected lookup file id %s", requestedFileId)
}
atomic.AddInt32(&lookupCount, 1)
return []string{url}, nil
}
var fetchCount int32
fetchFn := func(ctx context.Context, buffer []byte, urlStrings []string, cipherKey []byte, isGzipped bool, isFullChunk bool, offset int64, requestedFileId string) (int, error) {
atomic.AddInt32(&fetchCount, 1)
return 0, fmt.Errorf("fetch failed")
}
rc := NewReaderCache(10, cache, lookupFn, nil)
rc.fetchChunkDataFn = fetchFn
defer rc.destroy()
for i := 0; i < 2; i++ {
buffer := make([]byte, 8)
_, err := rc.ReadChunkAt(context.Background(), buffer, fileId, nil, false, 0, len(buffer), true)
if err == nil {
t.Fatalf("read %d should fail", i+1)
}
}
if got := atomic.LoadInt32(&lookupCount); got != 2 {
t.Fatalf("failed downloader should be removed so the second read re-lookups, got %d lookups", got)
}
if got := atomic.LoadInt32(&fetchCount); got != 2 {
t.Fatalf("failed downloader should be removed so the second read refetches, got %d fetches", got)
}
}
// TestReaderCacheContextCancellation tests that a reader can cancel its wait
// while the download continues for other readers
func TestReaderCacheContextCancellation(t *testing.T) {
@@ -67,7 +193,7 @@ func TestReaderCacheContextCancellation(t *testing.T) {
// Create a ReaderCache - we can't easily test the full flow without mocking HTTP,
// but we can test the context cancellation in readChunkAt
rc := NewReaderCache(10, cache, nil)
rc := NewReaderCache(10, cache, nil, nil)
defer rc.destroy()
// Pre-populate cache to avoid HTTP calls
@@ -106,7 +232,7 @@ func TestReaderCacheFallbackToChunkCache(t *testing.T) {
testData := []byte("fallback test data that should be found in chunk cache")
cache.SetChunk("fallback-file", testData)
rc := NewReaderCache(10, cache, nil)
rc := NewReaderCache(10, cache, nil, nil)
defer rc.destroy()
// Read should hit the chunk cache
@@ -138,7 +264,7 @@ func TestReaderCacheMultipleReadersWaitForSameChunk(t *testing.T) {
}
cache.SetChunk("shared-chunk", testData)
rc := NewReaderCache(10, cache, nil)
rc := NewReaderCache(10, cache, nil, nil)
defer rc.destroy()
// Launch multiple concurrent readers for the same chunk
@@ -184,7 +310,7 @@ func TestReaderCachePartialRead(t *testing.T) {
testData := []byte("0123456789ABCDEFGHIJ")
cache.SetChunk("partial-read-file", testData)
rc := NewReaderCache(10, cache, nil)
rc := NewReaderCache(10, cache, nil, nil)
defer rc.destroy()
tests := []struct {
@@ -222,7 +348,7 @@ func TestReaderCacheCleanup(t *testing.T) {
cache := newMockChunkCacheForReaderCache()
// Create cache with limit of 3
rc := NewReaderCache(3, cache, nil)
rc := NewReaderCache(3, cache, nil, nil)
defer rc.destroy()
// Add data for multiple files
@@ -259,7 +385,7 @@ func TestReaderCacheCleanup(t *testing.T) {
// TestSingleChunkCacherDoneSignal tests that done channel is always closed
func TestSingleChunkCacherDoneSignal(t *testing.T) {
cache := newMockChunkCacheForReaderCache()
rc := NewReaderCache(10, cache, nil)
rc := NewReaderCache(10, cache, nil, nil)
defer rc.destroy()
// Test that we can read even when data is in cache (done channel should work)
@@ -317,7 +443,7 @@ func TestSingleChunkCacherLookupError(t *testing.T) {
return nil, fmt.Errorf("lookup failed for %s", fileId)
}
rc := NewReaderCache(10, cache, lookupFn)
rc := NewReaderCache(10, cache, lookupFn, nil)
defer rc.destroy()
buffer := make([]byte, 100)
@@ -343,7 +469,7 @@ func TestSingleChunkCacherContextCancellationDuringLookup(t *testing.T) {
return nil, fmt.Errorf("lookup completed but reader should have cancelled")
}
rc := NewReaderCache(10, cache, lookupFn)
rc := NewReaderCache(10, cache, lookupFn, nil)
defer rc.destroy()
defer close(lookupCanFinish) // Ensure cleanup
@@ -391,7 +517,7 @@ func TestSingleChunkCacherMultipleReadersWaitForDownload(t *testing.T) {
return nil, fmt.Errorf("simulated lookup error")
}
rc := NewReaderCache(10, cache, lookupFn)
rc := NewReaderCache(10, cache, lookupFn, nil)
defer rc.destroy()
numReaders := 5
@@ -438,17 +564,25 @@ func TestSingleChunkCacherMultipleReadersWaitForDownload(t *testing.T) {
// the downloaders map deduplicates in-flight downloads.
func TestReaderCacheDownloaderDedup(t *testing.T) {
cache := newMockChunkCacheForReaderCache()
var lookupCount int32
lookupGate := make(chan struct{})
var fetchCount int32
fetchGate := make(chan struct{})
testData := []byte("deduplicated data")
lookupFn := func(ctx context.Context, fileId string) ([]string, error) {
atomic.AddInt32(&lookupCount, 1)
<-lookupGate
// Return an error so we don't need to mock the HTTP fetch.
return nil, fmt.Errorf("simulated lookup for %s", fileId)
return []string{"http://volume/" + fileId}, nil
}
rc := NewReaderCache(10, cache, lookupFn)
fetchFn := func(ctx context.Context, buffer []byte, urlStrings []string, cipherKey []byte, isGzipped bool, isFullChunk bool, offset int64, fileId string) (int, error) {
atomic.AddInt32(&fetchCount, 1)
<-fetchGate
return copy(buffer, testData), nil
}
rc := NewReaderCache(10, cache, lookupFn, nil)
rc.fetchChunkDataFn = fetchFn
defer rc.destroy()
const numReaders = 10
@@ -464,13 +598,15 @@ func TestReaderCacheDownloaderDedup(t *testing.T) {
}
// Allow downloads to proceed.
close(lookupGate)
close(fetchGate)
wg.Wait()
count := atomic.LoadInt32(&lookupCount)
if count != 1 {
if count := atomic.LoadInt32(&lookupCount); count != 1 {
t.Errorf("expected exactly 1 lookup call, got %d", count)
}
if count := atomic.LoadInt32(&fetchCount); count != 1 {
t.Errorf("expected exactly 1 fetch call, got %d", count)
}
}
// TestSingleChunkCacherOneReaderCancelsOthersContinue tests that when one reader
@@ -487,7 +623,7 @@ func TestSingleChunkCacherOneReaderCancelsOthersContinue(t *testing.T) {
return nil, fmt.Errorf("simulated error after delay")
}
rc := NewReaderCache(10, cache, lookupFn)
rc := NewReaderCache(10, cache, lookupFn, nil)
defer rc.destroy()
cancelledReaderDone := make(chan error, 1)
+38 -29
View File
@@ -102,10 +102,6 @@ func PrepareStreamContent(masterClient wdclient.HasLookupFileIdFunction, jwtFunc
type VolumeServerJwtFunction func(fileId string) string
type CacheInvalidator interface {
InvalidateCache(fileId string)
}
// urlSlicesEqual checks if two URL slices contain the same URLs (order-independent)
func urlSlicesEqual(a, b []string) bool {
if len(a) != len(b) {
@@ -126,6 +122,36 @@ func urlSlicesEqual(a, b []string) bool {
return true
}
// retryFetchWithFreshLocations is the shared self-heal for the read paths: when a chunk fetch
// fails, invalidate the cached volume locations, re-lookup, and call refetch only when the
// resolved locations actually changed (so we never retry against the same servers). originalErr
// is returned unchanged when no retry is attempted, so callers surface the real fetch failure.
func retryFetchWithFreshLocations(ctx context.Context, invalidator CacheInvalidator, lookupFn wdclient.LookupFileIdFunctionType, fileId string, oldUrls []string, originalErr error, refetch func(newUrls []string) error) error {
if invalidator == nil {
return originalErr
}
glog.V(0).InfofCtx(ctx, "read chunk %s failed, invalidating cache and retrying: %v", fileId, originalErr)
invalidator.InvalidateCache(fileId)
newUrls, lookupErr := lookupFn(ctx, fileId)
if lookupErr != nil {
glog.WarningfCtx(ctx, "failed to re-lookup chunk %s after cache invalidation: %v", fileId, lookupErr)
return fmt.Errorf("re-lookup chunk %s after cache invalidation: %w", fileId, lookupErr)
}
if len(newUrls) == 0 {
glog.WarningfCtx(ctx, "re-lookup for chunk %s returned no locations, skipping retry", fileId)
return fmt.Errorf("re-lookup chunk %s returned no locations", fileId)
}
if urlSlicesEqual(oldUrls, newUrls) {
glog.V(0).InfofCtx(ctx, "re-lookup returned same locations for chunk %s, skipping retry", fileId)
return originalErr
}
glog.V(0).InfofCtx(ctx, "retrying read chunk %s with %d new locations", fileId, len(newUrls))
return refetch(newUrls)
}
func PrepareStreamContentWithThrottler(ctx context.Context, masterClient wdclient.HasLookupFileIdFunction, jwtFunc VolumeServerJwtFunction, chunks []*filer_pb.FileChunk, offset int64, size int64, downloadMaxBytesPs int64) (DoStreamContent, error) {
glog.V(4).InfofCtx(ctx, "prepare to stream content for chunks: %d", len(chunks))
chunkViews := ViewFromChunks(ctx, masterClient.GetLookupFileIdFunction(), chunks, offset, size)
@@ -195,32 +221,15 @@ func PrepareStreamContentWithThrottler(ctx context.Context, masterClient wdclien
// If read failed, try to invalidate cache and re-lookup
if err != nil && written == 0 {
if invalidator, ok := masterClient.(CacheInvalidator); ok {
glog.V(0).InfofCtx(ctx, "read chunk %s failed, invalidating cache and retrying", chunkView.FileId)
invalidator.InvalidateCache(chunkView.FileId)
// Re-lookup
newUrlStrings, lookupErr := masterClient.GetLookupFileIdFunction()(ctx, chunkView.FileId)
if lookupErr == nil && len(newUrlStrings) > 0 {
// Check if new URLs are different from old ones to avoid infinite retry
if !urlSlicesEqual(urlStrings, newUrlStrings) {
glog.V(0).InfofCtx(ctx, "retrying read chunk %s with new locations: %v", chunkView.FileId, newUrlStrings)
_, err = retriedStreamFetchChunkData(ctx, writer, newUrlStrings, jwt, chunkView.CipherKey, chunkView.IsGzipped, chunkView.IsFullChunk(), chunkView.OffsetInChunk, int(chunkView.ViewSize))
// Update the map so subsequent references use fresh URLs
if err == nil {
fileId2Url[chunkView.FileId] = newUrlStrings
}
} else {
glog.V(0).InfofCtx(ctx, "re-lookup returned same locations for chunk %s, skipping retry", chunkView.FileId)
}
} else {
if lookupErr != nil {
glog.WarningfCtx(ctx, "failed to re-lookup chunk %s after cache invalidation: %v", chunkView.FileId, lookupErr)
} else {
glog.WarningfCtx(ctx, "re-lookup for chunk %s returned no locations, skipping retry", chunkView.FileId)
}
invalidator, _ := masterClient.(CacheInvalidator)
err = retryFetchWithFreshLocations(ctx, invalidator, masterClient.GetLookupFileIdFunction(), chunkView.FileId, urlStrings, err, func(newUrls []string) error {
_, refetchErr := retriedStreamFetchChunkData(ctx, writer, newUrls, jwt, chunkView.CipherKey, chunkView.IsGzipped, chunkView.IsFullChunk(), chunkView.OffsetInChunk, int(chunkView.ViewSize))
if refetchErr == nil {
// Update the map so subsequent references use fresh URLs
fileId2Url[chunkView.FileId] = newUrls
}
}
return refetchErr
})
}
offset += int64(chunkView.ViewSize)
+14 -34
View File
@@ -177,7 +177,7 @@ func streamChunksPrefetched(
consumeErr = err
break
}
retryErr := retryWithCacheInvalidation(localCtx, writer, chunkView, result.urlStrings, jwtFunc, masterClient)
retryErr := retryWithCacheInvalidation(localCtx, writer, chunkView, result.urlStrings, result.fetchErr, jwtFunc, masterClient)
if retryErr != nil {
stats.FilerHandlerCounter.WithLabelValues("chunkDownloadError").Inc()
consumeErr = fmt.Errorf("read chunk: %w", retryErr)
@@ -230,45 +230,25 @@ func streamChunksPrefetched(
return nil
}
// retryWithCacheInvalidation attempts to re-fetch a chunk after invalidating the URL cache.
// This mirrors the retry logic in PrepareStreamContentWithThrottler's sequential path.
// retryWithCacheInvalidation re-fetches a chunk via the shared location self-heal after the
// initial fetch failed with originalErr.
func retryWithCacheInvalidation(
ctx context.Context,
writer io.Writer,
chunkView *ChunkView,
oldUrlStrings []string,
originalErr error,
jwtFunc VolumeServerJwtFunction,
masterClient wdclient.HasLookupFileIdFunction,
) error {
invalidator, ok := masterClient.(CacheInvalidator)
if !ok {
return fmt.Errorf("read chunk %s failed and no cache invalidator available", chunkView.FileId)
}
glog.V(0).InfofCtx(ctx, "prefetch read chunk %s failed, invalidating cache and retrying", chunkView.FileId)
invalidator.InvalidateCache(chunkView.FileId)
newUrlStrings, lookupErr := masterClient.GetLookupFileIdFunction()(ctx, chunkView.FileId)
if lookupErr != nil {
glog.WarningfCtx(ctx, "failed to re-lookup chunk %s after cache invalidation: %v", chunkView.FileId, lookupErr)
return fmt.Errorf("re-lookup chunk %s: %w", chunkView.FileId, lookupErr)
}
if len(newUrlStrings) == 0 {
glog.WarningfCtx(ctx, "re-lookup for chunk %s returned no locations, skipping retry", chunkView.FileId)
return fmt.Errorf("re-lookup chunk %s: no locations", chunkView.FileId)
}
if urlSlicesEqual(oldUrlStrings, newUrlStrings) {
glog.V(0).InfofCtx(ctx, "re-lookup returned same locations for chunk %s, skipping retry", chunkView.FileId)
return fmt.Errorf("read chunk %s failed, same locations after cache invalidation", chunkView.FileId)
}
glog.V(0).InfofCtx(ctx, "retrying read chunk %s with new locations: %v", chunkView.FileId, newUrlStrings)
jwt := jwtFunc(chunkView.FileId)
_, err := retriedStreamFetchChunkData(
ctx, writer, newUrlStrings, jwt,
chunkView.CipherKey, chunkView.IsGzipped, chunkView.IsFullChunk(),
chunkView.OffsetInChunk, int(chunkView.ViewSize),
)
return err
invalidator, _ := masterClient.(CacheInvalidator)
return retryFetchWithFreshLocations(ctx, invalidator, masterClient.GetLookupFileIdFunction(), chunkView.FileId, oldUrlStrings, originalErr, func(newUrls []string) error {
jwt := jwtFunc(chunkView.FileId)
_, err := retriedStreamFetchChunkData(
ctx, writer, newUrls, jwt,
chunkView.CipherKey, chunkView.IsGzipped, chunkView.IsFullChunk(),
chunkView.OffsetInChunk, int(chunkView.ViewSize),
)
return err
})
}
+1 -1
View File
@@ -100,7 +100,7 @@ func GenParquetReadFunc(filerClient filer_pb.FilerClient, t topic.Topic, p topic
fileSize := filer.FileSize(entry)
visibleIntervals, _ := filer.NonOverlappingVisibleIntervals(context.Background(), lookupFileIdFn, entry.Chunks, 0, int64(fileSize))
chunkViews := filer.ViewFromVisibleIntervals(visibleIntervals, 0, int64(fileSize))
readerCache := filer.NewReaderCache(32, chunkCache, lookupFileIdFn)
readerCache := filer.NewReaderCache(32, chunkCache, lookupFileIdFn, nil)
readerAt := filer.NewChunkReaderAtFromClient(context.Background(), readerCache, chunkViews, int64(fileSize), filer.DefaultPrefetchCount)
// create parquet reader
+1 -1
View File
@@ -1166,7 +1166,7 @@ func (h *HybridMessageScanner) extractParquetFileStats(entry *filer_pb.Entry, lo
fileSize := filer.FileSize(entry)
visibleIntervals, _ := filer.NonOverlappingVisibleIntervals(context.Background(), lookupFileIdFn, entry.Chunks, 0, int64(fileSize))
chunkViews := filer.ViewFromVisibleIntervals(visibleIntervals, 0, int64(fileSize))
readerCache := filer.NewReaderCache(32, chunkCache, lookupFileIdFn)
readerCache := filer.NewReaderCache(32, chunkCache, lookupFileIdFn, nil)
readerAt := filer.NewChunkReaderAtFromClient(context.Background(), readerCache, chunkViews, int64(fileSize), filer.DefaultPrefetchCount)
// Create parquet reader - this only reads metadata, not data
+1 -1
View File
@@ -277,7 +277,7 @@ func NewS3ApiServerWithStore(router *mux.Router, option *S3ApiServerOption, expl
} else {
chunkCache = (*chunk_cache.TieredChunkCache)(nil)
}
readerCache := filer.NewReaderCache(s3ReaderCacheDownloaderLimit, chunkCache, filerClient.GetLookupFileIdFunction())
readerCache := filer.NewReaderCache(s3ReaderCacheDownloaderLimit, chunkCache, filerClient.GetLookupFileIdFunction(), filerClient)
s3ApiServer = &S3ApiServer{
option: option,
+1 -1
View File
@@ -137,7 +137,7 @@ func NewWebDavFileSystem(option *WebDavOption) (webdav.FileSystem, error) {
chunkCache: chunkCache,
signature: util.RandomInt32(),
}
t.readerCache = filer.NewReaderCache(32, chunkCache, filer.LookupFn(t))
t.readerCache = filer.NewReaderCache(32, chunkCache, filer.LookupFn(t), nil)
return t, nil
}