mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-02 10:17:10 +01:00
* 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>
291 lines
7.1 KiB
Go
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
|
|
}
|