Files
seaweedfs/weed/filer/filechunk_manifest.go
T
6840371774 sftp: keep a file's existing bytes when it is opened for write without truncate (#11723)
* sftp: keep a file's existing bytes when it is opened for write without truncate

* sftp: preserve sparse gaps and refuse unreadable entries when copying existing content

The copy used a single chunk-stream read, which ends at the first sparse
gap: the tail past a hole was dropped, and a remote-only entry with no
local chunks copied zero bytes, so closing the handle uploaded an empty
file. Read through PrepareStreamContent, which fills unstored regions
with zeros, and refuse entries that carry no local bytes at all.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* filer: report a failed destination write from a completed chunk fetch

retriedStreamFetchChunkData broke out of the retry loop on a clean HTTP
end before checking writeErr, so a destination write that failed mid-
stream returned a short write and nil error. Check writeErr first.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* sftp: cover remote and all-holes sizes when copying existing content

Take the declared size from RemoteEntry when it exceeds FileSize, pad
inline content to the declared size, and treat a chunkless local file as
all holes rather than unreadable. A remote-only entry still refuses the
copy, now checked with a zero FileSize as well.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* sftp: size the existing-content copy the way filer.FileSize does

A locally shortened file still names its older, larger remote object;
taking RemoteSize unconditionally padded the copy back out to it.
filer.FileSize only honors the remote size when the remote is newer,
and also covers the chunk-extent size. A remote-only entry still
refuses the copy whenever it points at remote bytes.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-13 07:20:20 +08:00

596 lines
20 KiB
Go

package filer
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"math"
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/wdclient"
"google.golang.org/protobuf/proto"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/security"
"github.com/seaweedfs/seaweedfs/weed/util"
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
)
var bytesBufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
// Keep Manifest reads bounded across all recursion levels of one resolution.
const maxChunkManifestResolveWorkers = 4
// Size of the job queue buffer. Large enough that submission does not block
// under normal chunk counts, so a promptly-failing manifest is always queued
// and can cancel stalled sibling reads once a worker picks it up.
const chunkManifestResolveJobBufferSize = 128
func HasChunkManifest(chunks []*filer_pb.FileChunk) bool {
for _, chunk := range chunks {
if chunk.IsChunkManifest {
return true
}
}
return false
}
func SeparateManifestChunks(chunks []*filer_pb.FileChunk) (manifestChunks, nonManifestChunks []*filer_pb.FileChunk) {
for _, c := range chunks {
if c.IsChunkManifest {
manifestChunks = append(manifestChunks, c)
} else {
nonManifestChunks = append(nonManifestChunks, c)
}
}
return
}
func ResolveChunkManifest(ctx context.Context, lookupFileIdFn wdclient.LookupFileIdFunctionType, chunks []*filer_pb.FileChunk, startOffset, stopOffset int64, invalidator CacheInvalidator) (dataChunks, manifestChunks []*filer_pb.FileChunk, manifestResolveErr error) {
resolver := newChunkManifestResolver(ctx, lookupFileIdFn, invalidator, nil)
defer resolver.close()
return resolver.resolve(chunks, startOffset, stopOffset)
}
// ResolveChunkManifestWithFilerJwt is ResolveChunkManifest that signs proxied
// manifest downloads with filerJwtFn instead of the process-wide filer read
// key, for readers carrying per-source credentials.
func ResolveChunkManifestWithFilerJwt(ctx context.Context, lookupFileIdFn wdclient.LookupFileIdFunctionType, filerJwtFn security.FilerJwtProvider, chunks []*filer_pb.FileChunk, startOffset, stopOffset int64, invalidator CacheInvalidator) (dataChunks, manifestChunks []*filer_pb.FileChunk, manifestResolveErr error) {
resolver := newChunkManifestResolver(ctx, lookupFileIdFn, invalidator, filerJwtFn)
defer resolver.close()
return resolver.resolve(chunks, startOffset, stopOffset)
}
type chunkManifestResolveJob struct {
chunk *filer_pb.FileChunk
result *chunkManifestResolveResult
done *sync.WaitGroup
batchCtx context.Context
batchCancel context.CancelFunc
batchOnce *sync.Once
}
type chunkManifestResolveResult struct {
chunks []*filer_pb.FileChunk
err error
internalCancel bool
}
type chunkManifestResolver struct {
ctx context.Context
parentCtx context.Context
cancel context.CancelFunc
lookupFileIdFn wdclient.LookupFileIdFunctionType
invalidator CacheInvalidator
filerJwtFn security.FilerJwtProvider
jobs chan chunkManifestResolveJob
overflowSem chan struct{}
workers sync.WaitGroup
startOnce sync.Once
started bool
}
func newChunkManifestResolver(ctx context.Context, lookupFileIdFn wdclient.LookupFileIdFunctionType, invalidator CacheInvalidator, filerJwtFn security.FilerJwtProvider) *chunkManifestResolver {
workCtx, cancel := context.WithCancel(ctx)
resolver := &chunkManifestResolver{
ctx: workCtx,
parentCtx: ctx,
cancel: cancel,
lookupFileIdFn: lookupFileIdFn,
invalidator: invalidator,
filerJwtFn: filerJwtFn,
jobs: make(chan chunkManifestResolveJob, chunkManifestResolveJobBufferSize),
overflowSem: make(chan struct{}, maxChunkManifestResolveWorkers),
}
return resolver
}
func (r *chunkManifestResolver) executeJob(job chunkManifestResolveJob) {
job.result.chunks, job.result.err = resolveOneChunkManifest(job.batchCtx, r.lookupFileIdFn, job.chunk, r.invalidator, nil, r.filerJwtFn)
if job.result.err != nil && r.parentCtx.Err() == nil {
if job.batchCtx.Err() != nil && errors.Is(job.result.err, context.Canceled) {
job.result.internalCancel = true
} else if job.batchCtx.Err() == nil {
job.batchOnce.Do(job.batchCancel)
}
}
job.done.Done()
}
func (r *chunkManifestResolver) executeOverflowJob(job chunkManifestResolveJob) {
select {
case r.overflowSem <- struct{}{}:
defer func() { <-r.overflowSem }()
r.executeJob(job)
case <-job.batchCtx.Done():
if job.result.err == nil {
job.result.err = job.batchCtx.Err()
job.result.internalCancel = r.parentCtx.Err() == nil && job.result.err != nil
}
job.done.Done()
case <-r.ctx.Done():
if job.result.err == nil {
job.result.err = r.ctx.Err()
job.result.internalCancel = r.parentCtx.Err() == nil && job.result.err != nil
}
job.done.Done()
}
}
func (r *chunkManifestResolver) worker() {
defer r.workers.Done()
for job := range r.jobs {
r.executeJob(job)
}
}
func (r *chunkManifestResolver) close() {
r.cancel()
close(r.jobs)
if r.started {
r.workers.Wait()
}
}
func (r *chunkManifestResolver) submit(job chunkManifestResolveJob) bool {
r.startOnce.Do(func() {
r.workers.Add(maxChunkManifestResolveWorkers)
for i := 0; i < maxChunkManifestResolveWorkers; i++ {
go r.worker()
}
r.started = true
})
select {
case r.jobs <- job:
return true
case <-r.ctx.Done():
return false
case <-job.batchCtx.Done():
// Batch was cancelled by a sibling failure; don't queue this job.
// Set the result so the caller doesn't overwrite it; the caller
// owns the WaitGroup decrement.
job.result.err = job.batchCtx.Err()
job.result.internalCancel = r.parentCtx.Err() == nil && job.result.err != nil
return false
default:
// Buffer is full (exceedingly rare: more than chunkManifestResolveJobBufferSize
// in-range manifests at one level). Run the job in a bounded overflow goroutine
// so a promptly-failing manifest can still cancel the batch without waiting for
// a worker slot, while keeping total concurrency bounded.
go r.executeOverflowJob(job)
return true
}
}
func (r *chunkManifestResolver) resolve(chunks []*filer_pb.FileChunk, startOffset, stopOffset int64) (dataChunks, manifestChunks []*filer_pb.FileChunk, manifestResolveErr error) {
type resolveSlot struct {
chunk *filer_pb.FileChunk
result chunkManifestResolveResult
}
// Cancellation is scoped to this parallel read batch so a failure in a
// later manifest does not cancel recursive work for an earlier manifest
// that already completed. Recursion creates its own batch context derived
// from the still-alive resolver context.
batchCtx, batchCancel := context.WithCancel(r.ctx)
defer batchCancel()
var batchOnce sync.Once
slots := make([]resolveSlot, len(chunks))
var reads sync.WaitGroup
for i, chunk := range chunks {
if max(chunk.Offset, startOffset) >= min(chunk.Offset+int64(chunk.Size), stopOffset) {
continue
}
slots[i].chunk = chunk
if !chunk.IsChunkManifest {
continue
}
reads.Add(1)
if !r.submit(chunkManifestResolveJob{
chunk: chunk,
result: &slots[i].result,
done: &reads,
batchCtx: batchCtx,
batchCancel: batchCancel,
batchOnce: &batchOnce,
}) {
// submit may have already set the result (batch cancellation).
// Only set it here for the resolver-cancellation case.
if slots[i].result.err == nil {
slots[i].result.err = r.ctx.Err()
slots[i].result.internalCancel = r.parentCtx.Err() == nil && slots[i].result.err != nil
}
reads.Done()
}
}
reads.Wait()
if err := r.parentCtx.Err(); err != nil {
return dataChunks, nil, err
}
// Recurse only after this level's reads release their worker slots. A
// worker must never wait for a child manifest while holding a slot.
//
// Pre-scan for the first real (non-internal-cancel) error before
// recursing. If a later manifest already failed, return its error
// promptly with data chunks that are already in hand, instead of
// blocking on recursive reads of earlier manifests' children.
for i, slot := range slots {
if slot.chunk == nil {
continue
}
if slot.result.err != nil {
if slot.result.internalCancel {
continue
}
for j := 0; j < i; j++ {
if slots[j].chunk == nil {
continue
}
if !slots[j].chunk.IsChunkManifest {
dataChunks = append(dataChunks, slots[j].chunk)
continue
}
for _, c := range slots[j].result.chunks {
if !c.IsChunkManifest && max(c.Offset, startOffset) < min(c.Offset+int64(c.Size), stopOffset) {
dataChunks = append(dataChunks, c)
}
}
}
return dataChunks, nil, slot.result.err
}
}
for _, slot := range slots {
if slot.chunk == nil {
continue
}
if slot.result.err != nil {
if slot.result.internalCancel {
continue
}
return dataChunks, nil, slot.result.err
}
if !slot.chunk.IsChunkManifest {
dataChunks = append(dataChunks, slot.chunk)
continue
}
manifestChunks = append(manifestChunks, slot.chunk)
subDataChunks, subManifestChunks, subErr := r.resolve(slot.result.chunks, startOffset, stopOffset)
if subErr != nil {
return dataChunks, nil, subErr
}
dataChunks = append(dataChunks, subDataChunks...)
manifestChunks = append(manifestChunks, subManifestChunks...)
}
return
}
// ResolveOneChunkManifest fetches and decodes a single manifest chunk. It is
// the uncached, exported path used by every non-Mount caller; the Mount path
// routes through resolveOneChunkManifest so it can share a per-mount cache.
// Keeping this signature stable preserves the existing four-argument contract
// for external callers.
func ResolveOneChunkManifest(ctx context.Context, lookupFileIdFn wdclient.LookupFileIdFunctionType, chunk *filer_pb.FileChunk, invalidator CacheInvalidator) (dataChunks []*filer_pb.FileChunk, manifestResolveErr error) {
return resolveOneChunkManifest(ctx, lookupFileIdFn, chunk, invalidator, nil, nil)
}
// ResolveOneChunkManifestWithFilerJwt is ResolveOneChunkManifest that signs
// proxied manifest downloads with filerJwtFn instead of the process-wide
// filer read key, for readers carrying per-source credentials.
func ResolveOneChunkManifestWithFilerJwt(ctx context.Context, lookupFileIdFn wdclient.LookupFileIdFunctionType, filerJwtFn security.FilerJwtProvider, chunk *filer_pb.FileChunk, invalidator CacheInvalidator) (dataChunks []*filer_pb.FileChunk, manifestResolveErr error) {
return resolveOneChunkManifest(ctx, lookupFileIdFn, chunk, invalidator, nil, filerJwtFn)
}
// resolveOneChunkManifest is the cache-aware implementation. cache may be nil,
// in which case the manifest is fetched and validated on every call, matching
// the historical uncached behavior. A non-nil cache is owned by a single mount
// (WFS) and coalesces concurrent cold misses via singleflight. filerJwtFn
// overrides the filer read credential for proxied downloads; nil means the
// process-wide key.
func resolveOneChunkManifest(ctx context.Context, lookupFileIdFn wdclient.LookupFileIdFunctionType, chunk *filer_pb.FileChunk, invalidator CacheInvalidator, cache *ChunkManifestCache, filerJwtFn security.FilerJwtProvider) (dataChunks []*filer_pb.FileChunk, manifestResolveErr error) {
if !chunk.IsChunkManifest {
return
}
if err := ctx.Err(); err != nil {
return nil, err
}
key := chunkManifestCacheKey{
fileID: chunk.GetFileIdString(),
cipherKey: string(chunk.CipherKey),
isCompressed: chunk.IsCompressed,
}
fetch := func() ([]byte, error) {
bytesBuffer := bytesBufferPool.Get().(*bytes.Buffer)
bytesBuffer.Reset()
defer bytesBufferPool.Put(bytesBuffer)
if err := fetchWholeChunk(ctx, bytesBuffer, lookupFileIdFn, key.fileID, chunk.CipherKey, chunk.IsCompressed, invalidator, filerJwtFn); err != nil {
return nil, fmt.Errorf("fail to read manifest %s: %w", key.fileID, err)
}
// Copy before the buffer returns to the pool so concurrent callers
// cannot overwrite the slice before it is cached.
data := append([]byte(nil), bytesBuffer.Bytes()...)
// Validate before returning so fetchOrLoad only caches well-formed
// manifests. A malformed manifest must not poison the cache.
if err := proto.Unmarshal(data, &filer_pb.FileChunkManifest{}); err != nil {
return nil, fmt.Errorf("fail to unmarshal manifest %s: %w", key.fileID, err)
}
return data, nil
}
var manifestBytes []byte
if cache != nil {
data, err := cache.fetchOrLoad(ctx, key, fetch)
if err != nil {
return nil, err
}
manifestBytes = data
} else {
data, err := fetch()
if err != nil {
return nil, err
}
manifestBytes = data
}
m := &filer_pb.FileChunkManifest{}
if err := proto.Unmarshal(manifestBytes, m); err != nil {
return nil, fmt.Errorf("fail to unmarshal manifest %s: %w", key.fileID, err)
}
// recursive
filer_pb.AfterEntryDeserialization(m.Chunks)
return m.Chunks, nil
}
func fetchWholeChunk(ctx context.Context, bytesBuffer *bytes.Buffer, lookupFileIdFn wdclient.LookupFileIdFunctionType, fileId string, cipherKey []byte, isGzipped bool, invalidator CacheInvalidator, filerJwtFn security.FilerJwtProvider) error {
urlStrings, err := lookupFileIdFn(ctx, fileId)
if err != nil {
glog.ErrorfCtx(ctx, "operation LookupFileId %s failed, err: %v", fileId, err)
return err
}
var jwt string
if filerJwtFn != nil && len(urlStrings) > 0 && util_http.IsProxyChunkUrl(urlStrings[0]) {
jwt = string(filerJwtFn(false))
} else {
jwt = ChunkReadJwt(urlStrings, fileId)
}
if _, err = retriedStreamFetchChunkData(ctx, bytesBuffer, urlStrings, jwt, cipherKey, isGzipped, true, 0, 0, refreshUrls(ctx, invalidator, lookupFileIdFn, fileId)); err == nil {
return nil
}
if ctxErr := ctx.Err(); ctxErr != nil {
// a cancelled read says nothing about where the volume lives, and the
// stream error it provoked is a symptom, not the cause
return ctxErr
}
return retryFetchWithFreshLocations(ctx, invalidator, lookupFileIdFn, fileId, urlStrings, err, func(newUrls []string) error {
// the failed attempt may have streamed a partial prefix into the buffer
bytesBuffer.Reset()
_, retryErr := retriedStreamFetchChunkData(ctx, bytesBuffer, newUrls, jwt, cipherKey, isGzipped, true, 0, 0, nil)
return retryErr
})
}
// retriedStreamFetchChunkData streams a chunk from the first location that
// answers. refreshUrls may be nil; when a location failed and a later one
// answered, it is called so the reads that follow start from a fresh list.
func retriedStreamFetchChunkData(ctx context.Context, writer io.Writer, urlStrings []string, jwt string, cipherKey []byte, isGzipped bool, isFullChunk bool, offset int64, size int, refreshUrls util_http.RefreshUrlsFunc) (written int64, err error) {
var shouldRetry bool
var totalWritten int
for waitTime := time.Second; waitTime < util.RetryWaitTime; waitTime += waitTime / 2 {
// Check for context cancellation before starting retry loop
select {
case <-ctx.Done():
return int64(totalWritten), ctx.Err()
default:
}
retriedCnt := 0
var failed bool
for _, urlString := range util_http.ReachableFirst(urlStrings) {
// Check for context cancellation before each volume server request
select {
case <-ctx.Done():
return int64(totalWritten), ctx.Err()
default:
}
retriedCnt++
var localProcessed int
var writeErr error
shouldRetry, err = util_http.ReadUrlAsStream(ctx, util_http.AppendQueryParameter(urlString, "readDeleted", "true"), jwt, cipherKey, isGzipped, isFullChunk, offset, size, func(data []byte) {
// Check for context cancellation during data processing
select {
case <-ctx.Done():
writeErr = ctx.Err()
return
default:
}
if totalWritten > localProcessed {
toBeSkipped := totalWritten - localProcessed
if len(data) <= toBeSkipped {
localProcessed += len(data)
return // skip if already processed
}
data = data[toBeSkipped:]
localProcessed += toBeSkipped
}
var writtenCount int
writtenCount, writeErr = writer.Write(data)
localProcessed += writtenCount
totalWritten += writtenCount
})
if writeErr != nil {
err = writeErr
break
}
if !shouldRetry {
break
}
if err != nil {
failed = true
glog.V(0).InfofCtx(ctx, "read %s failed, err: %v", urlString, err)
} else {
break
}
}
if err == nil && failed && refreshUrls != nil {
refreshUrls()
}
// all nodes have tried it
if retriedCnt == len(urlStrings) {
break
}
if err != nil && shouldRetry {
glog.V(0).InfofCtx(ctx, "retry reading in %v", waitTime)
// Sleep with proper context cancellation and timer cleanup
timer := time.NewTimer(waitTime)
select {
case <-ctx.Done():
timer.Stop()
return int64(totalWritten), ctx.Err()
case <-timer.C:
// Continue with retry
}
} else {
break
}
}
return int64(totalWritten), err
}
// MaybeManifestize folds a flat chunk list into manifest chunks once it passes
// ManifestBatch, so an entry's chunk list stays within what the metadata store
// will hold. A fold that fails partway returns inputChunks unchanged -- never
// the half-folded list, which drops the manifests the caller came in with --
// and hands the blobs it had already saved to deleteChunks, since the flat list
// it returns references none of them. deleteChunks may be nil where the caller
// has no deleter to offer; then the blobs are only named in the log.
func MaybeManifestize(saveFunc SaveDataAsChunkFunctionType, deleteChunks func([]*filer_pb.FileChunk), inputChunks []*filer_pb.FileChunk) (chunks []*filer_pb.FileChunk, err error) {
var saved []*filer_pb.FileChunk
record := func(reader io.Reader, name string, offset int64, tsNs int64, expectedDataSize uint64) (*filer_pb.FileChunk, error) {
chunk, saveErr := saveFunc(reader, name, offset, tsNs, expectedDataSize)
if saveErr == nil {
saved = append(saved, chunk)
}
return chunk, saveErr
}
chunks, err = doMaybeManifestize(record, inputChunks, ManifestBatch, mergeIntoManifest)
if err == nil {
return chunks, nil
}
if len(saved) > 0 {
if deleteChunks != nil {
deleteChunks(saved)
} else {
glog.V(0).Infof("manifestize failed, %d manifest blobs left unreferenced: %v", len(saved), err)
}
}
return inputChunks, err
}
func doMaybeManifestize(saveFunc SaveDataAsChunkFunctionType, inputChunks []*filer_pb.FileChunk, mergeFactor int, mergefn func(saveFunc SaveDataAsChunkFunctionType, dataChunks []*filer_pb.FileChunk) (manifestChunk *filer_pb.FileChunk, err error)) (chunks []*filer_pb.FileChunk, err error) {
var dataChunks []*filer_pb.FileChunk
for _, chunk := range inputChunks {
if !chunk.IsChunkManifest {
dataChunks = append(dataChunks, chunk)
} else {
chunks = append(chunks, chunk)
}
}
remaining := len(dataChunks)
for i := 0; i+mergeFactor <= len(dataChunks); i += mergeFactor {
chunk, err := mergefn(saveFunc, dataChunks[i:i+mergeFactor])
if err != nil {
// dataChunks is what is left after the manifests the caller
// already had were separated out; returning it would drop them
return inputChunks, err
}
chunks = append(chunks, chunk)
remaining -= mergeFactor
}
// remaining
for i := len(dataChunks) - remaining; i < len(dataChunks); i++ {
chunks = append(chunks, dataChunks[i])
}
return
}
func mergeIntoManifest(saveFunc SaveDataAsChunkFunctionType, dataChunks []*filer_pb.FileChunk) (manifestChunk *filer_pb.FileChunk, err error) {
filer_pb.BeforeEntrySerialization(dataChunks)
// create and serialize the manifest
data, serErr := proto.Marshal(&filer_pb.FileChunkManifest{
Chunks: dataChunks,
})
if serErr != nil {
return nil, fmt.Errorf("serializing manifest: %w", serErr)
}
minOffset, maxOffset := int64(math.MaxInt64), int64(math.MinInt64)
for _, chunk := range dataChunks {
if minOffset > int64(chunk.Offset) {
minOffset = chunk.Offset
}
if maxOffset < int64(chunk.Size)+chunk.Offset {
maxOffset = int64(chunk.Size) + chunk.Offset
}
}
manifestChunk, err = saveFunc(bytes.NewReader(data), "", 0, 0, uint64(len(data)))
if err != nil {
return nil, err
}
manifestChunk.IsChunkManifest = true
manifestChunk.Offset = minOffset
manifestChunk.Size = uint64(maxOffset - minOffset)
return
}
type SaveDataAsChunkFunctionType func(reader io.Reader, name string, offset int64, tsNs int64, expectedDataSize uint64) (chunk *filer_pb.FileChunk, err error)