mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-22 22:11:39 +02:00
* filer.sync: sign proxied chunk I/O from the per-side security file The -a.security / -b.security files were used for gRPC TLS and the HTTPS client but not for jwt.filer_signing, so filer-proxied chunk reads and writes carried a token signed with the process-wide key and failed authorization whenever the two clusters' keys differ. LoadFilerJwtFromFile returns a FilerJwtProvider for each side's file, which FilerSource and FilerSink now accept for proxied chunk reads and writes. With no keys in the file or no flag, both fall back to the process-wide jwt.filer_signing configuration as before. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * replication: use the side filer read key for manifest downloads and fall back per access level Manifest chunk resolution still signed proxied downloads with the process-wide read key, so a source filer requiring its own key 401'd on manifest-bearing files. A side security file that set only one access level also produced empty tokens for the other instead of inheriting the process-wide key, and the side file loader ignored the WEED_ environment overrides the filer itself honors. ResolveChunkManifest/ResolveOneChunkManifest keep their signatures; FilerJwt-aware variants thread the provider down to fetchWholeChunk, which prefers it on proxy URLs. The side loader now applies the same environment precedence and falls back to the process-wide signer per missing access level. * security: verify the configured filer token lifetimes * security: reject negative filer token lifetimes A negative expires_after_seconds reached GenJwtForFilerServer and produced a token with no expiration claim. Also synchronize the Authorization-header capture in the proxy test and restore the prior viper key on cleanup. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@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 !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)
|