mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-27 07:20:44 +01:00
* filer: keep a ranged read in random mode through its contiguous tail A far ReadAt on a fresh ReaderPattern left the sequential counter at -1, so the next buffer of the same ranged request landed on the frontier and flipped the verdict straight back to sequential — readChunkSliceAt then paid a whole-chunk fetch for the remainder of the range. Drop the counter to -ModeChangeLimit when random mode is entered so the verdict needs sustained sequential evidence to undo, matching the hysteresis an established sequential stream already gets. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3: pin small ranged GETs to range reads A ranged GET whose first read lands within SeqTolerance of offset 0 is judged sequential immediately, and even a far-starting range could flip back mid-request; either way readChunkSliceAt downloads each covered chunk in full, multiplying disk reads for small ranged reads (measured ~7x). Pin random mode for ranged requests no larger than SeqTolerance so all of the request's buffer reads stay range fetches. Larger ranges keep the dynamic pattern, where whole-chunk fetches amortize. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: fetch only the part of a chunk the view covers Replaces the PinRandomMode size heuristic with a per-chunk coverage rule. ViewFromVisibleIntervals already clips chunk views to the request window, so a view that is not IsFullChunk() is one the request only partially needs; fetch it as a range regardless of the detected read pattern. This closes the holes a request-size pin left open: ranges larger than SeqTolerance no longer revert to whole-chunk downloads once their buffers look sequential, and ranges that fully cover a chunk keep the shared whole-chunk path instead of fetching 256KiB slices piecemeal. Prefetch (MaybeCache) skips clipped views so it cannot amplify a range read either. PinRandomMode is dropped: no caller needs it once coverage drives the fetch choice. Range fetches route through fetchChunkDataFn so tests observe them the same way as whole-chunk downloads. * filer: keep ciphered chunks on the whole-chunk path A range fetch cannot save bytes for a ciphered chunk: readEncryptedUrl always downloads and decrypts the whole blob before slicing. Sending partial views of ciphered chunks through fetchChunkRange would repeat the full download per buffer, so they keep the shared whole-chunk path where one download serves every buffer. Prefetch stays enabled for them for the same reason. * filer: keep compressed chunks on the whole-chunk path Like ciphered chunks, a range request on a compressed chunk makes the volume server read and decompress the whole needle, so range-per-buffer would repeat the full backend read for each 256KiB window. Route them through the shared whole-chunk path via ChunkView.CanRangeFetch. * filer: fall back to range fetch when a chunk exceeds the reader budget A ciphered or compressed chunk larger than readerCacheSizeMB can never be read through the whole-chunk path — the budget rejects the buffer — so its partial views must still range-fetch or the GET fails outright. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
570 lines
18 KiB
Go
570 lines
18 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/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)
|
|
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
|
|
jobs chan chunkManifestResolveJob
|
|
overflowSem chan struct{}
|
|
workers sync.WaitGroup
|
|
startOnce sync.Once
|
|
started bool
|
|
}
|
|
|
|
func newChunkManifestResolver(ctx context.Context, lookupFileIdFn wdclient.LookupFileIdFunctionType, invalidator CacheInvalidator) *chunkManifestResolver {
|
|
workCtx, cancel := context.WithCancel(ctx)
|
|
resolver := &chunkManifestResolver{
|
|
ctx: workCtx,
|
|
parentCtx: ctx,
|
|
cancel: cancel,
|
|
lookupFileIdFn: lookupFileIdFn,
|
|
invalidator: invalidator,
|
|
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)
|
|
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)
|
|
}
|
|
|
|
// 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.
|
|
func resolveOneChunkManifest(ctx context.Context, lookupFileIdFn wdclient.LookupFileIdFunctionType, chunk *filer_pb.FileChunk, invalidator CacheInvalidator, cache *ChunkManifestCache) (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); 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) error {
|
|
urlStrings, err := lookupFileIdFn(ctx, fileId)
|
|
if err != nil {
|
|
glog.ErrorfCtx(ctx, "operation LookupFileId %s failed, err: %v", fileId, err)
|
|
return err
|
|
}
|
|
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 !shouldRetry {
|
|
break
|
|
}
|
|
if writeErr != nil {
|
|
err = writeErr
|
|
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)
|