mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-19 20:44:28 +02:00
* 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>
596 lines
20 KiB
Go
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)
|