From 578fed7d301402c85a24f92f2431a9502d3dcd04 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 27 Apr 2026 01:34:40 -0700 Subject: [PATCH] extend cache-not-ready handling to filer HTTP path Mirror the s3api change for the native filer HTTP handlers. When the filer GET hits a remote-only object whose cache fill hasn't completed, return 503 Service Unavailable with Retry-After: 5 instead of 500 Internal Error, and treat client disconnects as silent cancellations rather than logging them as errors. Adds an ErrCacheNotReady sentinel and a small helper used at the prepareWriteFn-error sites in ProcessRangeRequest, so the same classification (cancel / not-ready / other) applies to plain GETs, single-range, and multi-range requests. --- weed/server/common.go | 36 +++++++++++++++++------ weed/server/filer_server_handlers_read.go | 9 ++++-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/weed/server/common.go b/weed/server/common.go index 2d3c55466f..712653188c 100644 --- a/weed/server/common.go +++ b/weed/server/common.go @@ -3,6 +3,7 @@ package weed_server import ( "bufio" "bytes" + "context" "encoding/json" "errors" "fmt" @@ -39,6 +40,28 @@ var writePool = sync.Pool{New: func() interface{} { }, } +// ErrCacheNotReady signals that a remote-only object's local cache is still +// filling. Callers should map it to 503 + Retry-After so SDKs back off and retry. +var ErrCacheNotReady = errors.New("remote object not cached yet") + +// writePrepareWriteFnErr writes an HTTP response for an error from +// prepareWriteFn, before any 2xx headers have been written. Client cancels are +// silent; ErrCacheNotReady becomes 503 + Retry-After; everything else is 500. +func writePrepareWriteFnErr(w http.ResponseWriter, err error) { + w.Header().Del("Content-Length") + switch { + case errors.Is(err, context.Canceled): + glog.V(3).Infof("ProcessRangeRequest: client disconnected: %v", err) + case errors.Is(err, ErrCacheNotReady): + glog.V(1).Infof("ProcessRangeRequest: cache not ready, returning 503: %v", err) + w.Header().Set("Retry-After", "5") + http.Error(w, err.Error(), http.StatusServiceUnavailable) + default: + glog.Errorf("ProcessRangeRequest: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + func init() { serverStats = stats.NewServerStats() go serverStats.Start() @@ -290,9 +313,7 @@ func ProcessRangeRequest(r *http.Request, w http.ResponseWriter, totalSize int64 w.Header().Set("Content-Length", strconv.FormatInt(totalSize, 10)) writeFn, err := prepareWriteFn(0, totalSize) if err != nil { - glog.Errorf("ProcessRangeRequest: %v", err) - w.Header().Del("Content-Length") - http.Error(w, err.Error(), http.StatusInternalServerError) + writePrepareWriteFnErr(w, err) return fmt.Errorf("ProcessRangeRequest: %w", err) } if err = writeFn(bufferedWriter); err != nil { @@ -340,9 +361,7 @@ func ProcessRangeRequest(r *http.Request, w http.ResponseWriter, totalSize int64 writeFn, err := prepareWriteFn(ra.start, ra.length) if err != nil { - glog.Errorf("ProcessRangeRequest range[0]: %+v err: %v", w.Header(), err) - w.Header().Del("Content-Length") - http.Error(w, err.Error(), http.StatusInternalServerError) + writePrepareWriteFnErr(w, err) return fmt.Errorf("ProcessRangeRequest: %w", err) } w.WriteHeader(http.StatusPartialContent) @@ -365,9 +384,8 @@ func ProcessRangeRequest(r *http.Request, w http.ResponseWriter, totalSize int64 } writeFn, err := prepareWriteFn(ra.start, ra.length) if err != nil { - glog.Errorf("ProcessRangeRequest range[%d] err: %v", i, err) - http.Error(w, "Internal Error", http.StatusInternalServerError) - return fmt.Errorf("ProcessRangeRequest range[%d] err: %v", i, err) + writePrepareWriteFnErr(w, err) + return fmt.Errorf("ProcessRangeRequest range[%d]: %w", i, err) } writeFnByRange[i] = writeFn } diff --git a/weed/server/filer_server_handlers_read.go b/weed/server/filer_server_handlers_read.go index 61a87cd972..c20adeebf5 100644 --- a/weed/server/filer_server_handlers_read.go +++ b/weed/server/filer_server_handlers_read.go @@ -211,8 +211,13 @@ func (fs *FilerServer) GetOrHeadHandler(w http.ResponseWriter, r *http.Request) Name: name, }); err != nil { stats.FilerHandlerCounter.WithLabelValues(stats.ErrorReadCache).Inc() - glog.ErrorfCtx(ctx, "CacheRemoteObjectToLocalCluster %s: %v", entry.FullPath, err) - return nil, fmt.Errorf("cache %s: %v", entry.FullPath, err) + // Client disconnected: surface ctx error so caller stays silent. + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, ctxErr + } + // Cache still filling: tag with sentinel so caller maps to 503 + Retry-After. + glog.WarningfCtx(ctx, "CacheRemoteObjectToLocalCluster %s: %v", entry.FullPath, err) + return nil, fmt.Errorf("cache %s: %w", entry.FullPath, ErrCacheNotReady) } else { chunks = resp.Entry.GetChunks() }