Files
seaweedfs/weed/operation/chunked_file.go
T
Chris LuandClaude Opus 5.5 5cf053c32e volume: read a manifest chunk from another replica when one fails (#12313)
* operation: read a manifest chunk from the next replica when one fails

ChunkedFileReader read each chunk from one random location and returned
the first error, so a chunk whose cached replica went down failed the
whole file although another replica had it. Try the chunk's locations in
turn (local-data replicas first) and resume each attempt at the byte the
previous one reached. A failed write to the caller still ends the read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* operation: drop a chunk volume's cached locations after a failed read

A chunk read that failed on a replica left that replica in the
10-minute location cache, so later reads kept trying it first. Drop the
volume's cached locations after a failure, and when every cached
replica has failed, ask the master once more and try the locations it
now lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* operation: don't continue a partly sent chunk on another replica

A volume server streams a large needle before it has verified the
checksum and aborts the response on a mismatch, so bytes that already
reached the reader may be corrupt. Continuing such a chunk on another
replica would hand back a mix of bad and good bytes as a complete read.
Move to the next replica only while nothing of the chunk has been
written; a mid-body failure still fails the read, and the dropped cache
entry lets the next read pick a live replica.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* operation: treat a short chunk body as a failed replica read

A replica that ends a chunk's body cleanly before the chunk's size made
the read look successful, so the next replica was never tried and the
following chunks landed at the wrong offsets. Read exactly the chunk's
remaining length and report a short body as an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 16:31:22 -08:00

291 lines
7.1 KiB
Go

package operation
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"sort"
"strings"
"sync"
"google.golang.org/grpc"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/security"
"github.com/seaweedfs/seaweedfs/weed/util"
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
)
var (
// when the remote server does not allow range requests (Accept-Ranges was not set)
ErrRangeRequestsNotSupported = errors.New("Range requests are not supported by the remote server")
// ErrInvalidRange is returned by Read when trying to read past the end of the file
ErrInvalidRange = errors.New("Invalid range")
)
type ChunkInfo struct {
Fid string `json:"fid"`
Offset int64 `json:"offset"`
Size int64 `json:"size"`
}
type ChunkList []*ChunkInfo
type ChunkManifest struct {
Name string `json:"name,omitempty"`
Mime string `json:"mime,omitempty"`
Size int64 `json:"size,omitempty"`
Chunks ChunkList `json:"chunks,omitempty"`
}
// seekable chunked file reader
type ChunkedFileReader struct {
totalSize int64
chunkList []*ChunkInfo
master pb.ServerAddress
pos int64
pr *io.PipeReader
pw *io.PipeWriter
mutex sync.Mutex
grpcDialOption grpc.DialOption
}
func (s ChunkList) Len() int { return len(s) }
func (s ChunkList) Less(i, j int) bool { return s[i].Offset < s[j].Offset }
func (s ChunkList) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func LoadChunkManifest(buffer []byte, isCompressed bool) (*ChunkManifest, error) {
if isCompressed {
var err error
if buffer, err = util.DecompressData(buffer); err != nil {
glog.V(0).Infof("fail to decompress chunk manifest: %v", err)
}
}
cm := ChunkManifest{}
if e := json.Unmarshal(buffer, &cm); e != nil {
return nil, e
}
sort.Sort(cm.Chunks)
return &cm, nil
}
func (cm *ChunkManifest) Marshal() ([]byte, error) {
return json.Marshal(cm)
}
func (cm *ChunkManifest) DeleteChunks(masterFn GetMasterFn, usePublicUrl bool, grpcDialOption grpc.DialOption) error {
var fileIds []string
for _, ci := range cm.Chunks {
fileIds = append(fileIds, ci.Fid)
}
results := DeleteFileIds(masterFn, usePublicUrl, grpcDialOption, fileIds)
// Check for any errors in results
for _, result := range results {
if result.Error != "" {
glog.V(0).Infof("delete file %+v: %v", result.FileId, result.Error)
return fmt.Errorf("chunk delete %v: %v", result.FileId, result.Error)
}
}
return nil
}
func readChunkNeedle(fileUrl string, w io.Writer, offset int64, size int64, jwt string) (written int64, e error) {
req, err := http.NewRequest(http.MethodGet, fileUrl, nil)
if err != nil {
return written, err
}
if offset > 0 {
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset))
}
if jwt != "" {
req.Header.Set("Authorization", security.BearerPrefix+jwt)
}
resp, err := util_http.Do(req)
if err != nil {
return written, err
}
defer util_http.CloseResponse(resp)
switch resp.StatusCode {
case http.StatusRequestedRangeNotSatisfiable:
return written, ErrInvalidRange
case http.StatusOK:
if offset > 0 {
return written, ErrRangeRequestsNotSupported
}
case http.StatusPartialContent:
break
default:
return written, util_http.StatusError(fileUrl, resp)
}
written, e = io.CopyN(w, resp.Body, size)
if e == io.EOF {
e = fmt.Errorf("%s: %d of %d bytes: %w", fileUrl, written, size, io.ErrUnexpectedEOF)
}
return written, e
}
func NewChunkedFileReader(chunkList []*ChunkInfo, master pb.ServerAddress, grpcDialOption grpc.DialOption) *ChunkedFileReader {
var totalSize int64
for _, chunk := range chunkList {
totalSize += chunk.Size
}
sort.Sort(ChunkList(chunkList))
return &ChunkedFileReader{
totalSize: totalSize,
chunkList: chunkList,
master: master,
grpcDialOption: grpcDialOption,
}
}
func (cf *ChunkedFileReader) Seek(offset int64, whence int) (int64, error) {
var err error
switch whence {
case io.SeekStart:
case io.SeekCurrent:
offset += cf.pos
case io.SeekEnd:
offset = cf.totalSize + offset
}
if offset > cf.totalSize {
err = ErrInvalidRange
}
if cf.pos != offset {
cf.Close()
}
cf.pos = offset
return cf.pos, err
}
func (cf *ChunkedFileReader) WriteTo(w io.Writer) (n int64, err error) {
chunkIndex := -1
chunkStartOffset := int64(0)
for i, ci := range cf.chunkList {
if cf.pos >= ci.Offset && cf.pos < ci.Offset+ci.Size {
chunkIndex = i
chunkStartOffset = cf.pos - ci.Offset
break
}
}
if chunkIndex < 0 {
return n, ErrInvalidRange
}
for ; chunkIndex < len(cf.chunkList); chunkIndex++ {
wn, e := cf.readChunk(cf.chunkList[chunkIndex], w, chunkStartOffset)
n += wn
cf.pos += wn
if e != nil {
return n, e
}
chunkStartOffset = 0
}
return n, nil
}
// readChunk reads the chunk from the first replica that serves it. A failure
// drops the volume's cached locations, and once every cached replica has
// failed the master is asked again. A replica that already sent part of the
// chunk is not continued elsewhere: a volume server streams a needle before
// its checksum is verified, so those bytes may be the corrupt ones.
func (cf *ChunkedFileReader) readChunk(ci *ChunkInfo, w io.Writer, offset int64) (written int64, err error) {
masterFn := func(_ context.Context) pb.ServerAddress {
return cf.master
}
vid, _, _ := strings.Cut(ci.Fid, ",")
dst := &writeErrorRecorder{Writer: w}
tried := make(map[string]bool)
for range 2 {
fileUrls, jwt, lookupError := lookupFileIdUrls(masterFn, cf.grpcDialOption, ci.Fid)
if lookupError != nil {
if err == nil {
err = lookupError
}
return 0, err
}
for _, fileUrl := range fileUrls {
if tried[fileUrl] {
continue
}
tried[fileUrl] = true
written, err = readChunkNeedle(fileUrl, dst, offset, ci.Size-offset, jwt)
if err == nil || dst.err != nil {
return written, err
}
glog.V(0).Infof("read chunk %s from %s: %v", ci.Fid, fileUrl, err)
InvalidateVolumeIdLocationCache(vid)
if written > 0 {
return written, err
}
}
}
return 0, err
}
type writeErrorRecorder struct {
io.Writer
err error
}
func (r *writeErrorRecorder) Write(p []byte) (int, error) {
n, err := r.Writer.Write(p)
if err != nil {
r.err = err
}
return n, err
}
func (cf *ChunkedFileReader) ReadAt(p []byte, off int64) (n int, err error) {
cf.Seek(off, 0)
return cf.Read(p)
}
func (cf *ChunkedFileReader) Read(p []byte) (int, error) {
return cf.getPipeReader().Read(p)
}
func (cf *ChunkedFileReader) Close() (e error) {
cf.mutex.Lock()
defer cf.mutex.Unlock()
return cf.closePipe()
}
func (cf *ChunkedFileReader) closePipe() (e error) {
if cf.pr != nil {
if err := cf.pr.Close(); err != nil {
e = err
}
}
cf.pr = nil
if cf.pw != nil {
if err := cf.pw.Close(); err != nil {
e = err
}
}
cf.pw = nil
return e
}
func (cf *ChunkedFileReader) getPipeReader() io.Reader {
cf.mutex.Lock()
defer cf.mutex.Unlock()
if cf.pr != nil && cf.pw != nil {
return cf.pr
}
cf.closePipe()
cf.pr, cf.pw = io.Pipe()
go func(pw *io.PipeWriter) {
_, e := cf.WriteTo(pw)
pw.CloseWithError(e)
}(cf.pw)
return cf.pr
}