Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
710 lines
24 KiB
Go
710 lines
24 KiB
Go
package filersink
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/schollz/progressbar/v3"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/replication/source"
|
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
|
)
|
|
|
|
// missingSourceChunkGrace is how long a chunk the source cannot produce keeps
|
|
// being retried before it counts as gone for good. Long enough to outlast a
|
|
// volume server restart or a master failover, after which every volume that
|
|
// still exists is registered again.
|
|
var missingSourceChunkGrace = 30 * time.Minute
|
|
|
|
// errSourceChunkMissing is a permanent replication failure: the source cluster
|
|
// no longer holds the chunk's data, so no later attempt can fetch it.
|
|
var errSourceChunkMissing = errors.New("source chunk missing")
|
|
|
|
// isSourceChunkMissing reports whether the source answered that it does not have
|
|
// the chunk — no location for its volume, or a 404 from the volume server that
|
|
// does. Both shapes also cover a volume that is merely offline, which is why the
|
|
// gate below waits them out instead of trusting a single answer.
|
|
func isSourceChunkMissing(err error) bool {
|
|
return errors.Is(err, source.ErrVolumeNotFound) || errors.Is(err, util_http.ErrNotFound)
|
|
}
|
|
|
|
// missingSourceChunkGate decides when to stop retrying a chunk the source cannot
|
|
// produce, so a lookup race or a restarting volume server is waited out while a
|
|
// vacuumed one is written off instead of retried forever. The wait is kept per
|
|
// volume on the sink: a volume that is gone took every file it held with it, and
|
|
// waiting it out once per file would stall the sync for as long as it holds files.
|
|
type missingSourceChunkGate struct {
|
|
sink *FilerSink
|
|
volumeId string
|
|
gaveUp bool
|
|
}
|
|
|
|
func (g *missingSourceChunkGate) isPermanent(err error) bool {
|
|
if !isSourceChunkMissing(err) {
|
|
return false
|
|
}
|
|
if time.Since(g.sink.sourceVolumeMissingSince(g.volumeId)) < missingSourceChunkGrace {
|
|
return false
|
|
}
|
|
g.gaveUp = true
|
|
return true
|
|
}
|
|
|
|
// wrap marks err permanent once the gate gave up, so the sink can tell a source
|
|
// that lost the data from one that is only unreachable right now.
|
|
func (g *missingSourceChunkGate) wrap(err error) error {
|
|
if err == nil || !g.gaveUp {
|
|
return err
|
|
}
|
|
return fmt.Errorf("%w: %w", errSourceChunkMissing, err)
|
|
}
|
|
|
|
func (fs *FilerSink) replicateChunks(ctx context.Context, sourceChunks []*filer_pb.FileChunk, path string, sourceMtimeNs int64) (replicatedChunks []*filer_pb.FileChunk, err error) {
|
|
if len(sourceChunks) == 0 {
|
|
return
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// a simple progress bar. Not ideal. Fix me.
|
|
var bar *progressbar.ProgressBar
|
|
if len(sourceChunks) > 1 {
|
|
name := filepath.Base(path)
|
|
bar = progressbar.NewOptions64(int64(len(sourceChunks)),
|
|
progressbar.OptionClearOnFinish(),
|
|
progressbar.OptionOnCompletion(func() {
|
|
fmt.Fprint(os.Stderr, "\n")
|
|
}),
|
|
progressbar.OptionFullWidth(),
|
|
progressbar.OptionSetDescription(name),
|
|
)
|
|
}
|
|
|
|
replicatedChunks = make([]*filer_pb.FileChunk, len(sourceChunks))
|
|
var errLock sync.Mutex
|
|
setError := func(e error) {
|
|
if e == nil {
|
|
return
|
|
}
|
|
errLock.Lock()
|
|
if err == nil {
|
|
err = e
|
|
}
|
|
errLock.Unlock()
|
|
}
|
|
hasError := func() bool {
|
|
errLock.Lock()
|
|
defer errLock.Unlock()
|
|
return err != nil
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for chunkIndex, sourceChunk := range sourceChunks {
|
|
if hasError() {
|
|
break
|
|
}
|
|
|
|
if sourceChunk.IsChunkManifest {
|
|
replicatedChunk, replicateErr := fs.replicateOneManifestChunk(ctx, sourceChunk, path, sourceMtimeNs)
|
|
if replicateErr != nil {
|
|
setError(replicateErr)
|
|
break
|
|
}
|
|
replicatedChunks[chunkIndex] = replicatedChunk
|
|
if bar != nil {
|
|
bar.Add(1)
|
|
}
|
|
continue
|
|
}
|
|
|
|
wg.Add(1)
|
|
index, source := chunkIndex, sourceChunk
|
|
fs.executor.Execute(func() {
|
|
defer wg.Done()
|
|
var replicatedChunk *filer_pb.FileChunk
|
|
retryErr := util.Retry("replicate chunks", func() error {
|
|
chunk, e := fs.replicateOneChunk(source, path, sourceMtimeNs)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
replicatedChunk = chunk
|
|
return nil
|
|
})
|
|
if retryErr != nil {
|
|
setError(retryErr)
|
|
return
|
|
}
|
|
|
|
replicatedChunks[index] = replicatedChunk
|
|
if bar != nil {
|
|
bar.Add(1)
|
|
}
|
|
})
|
|
}
|
|
wg.Wait()
|
|
|
|
return
|
|
}
|
|
|
|
func (fs *FilerSink) replicateOneChunk(sourceChunk *filer_pb.FileChunk, path string, sourceMtimeNs int64) (*filer_pb.FileChunk, error) {
|
|
|
|
fileId, err := fs.fetchAndWrite(sourceChunk, path, sourceMtimeNs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("copy %s: %w", sourceChunk.GetFileIdString(), err)
|
|
}
|
|
|
|
return &filer_pb.FileChunk{
|
|
FileId: fileId,
|
|
Offset: sourceChunk.Offset,
|
|
Size: sourceChunk.Size,
|
|
ModifiedTsNs: sourceChunk.ModifiedTsNs,
|
|
ETag: sourceChunk.ETag,
|
|
SourceFileId: sourceChunk.GetFileIdString(),
|
|
CipherKey: sourceChunk.CipherKey,
|
|
IsCompressed: sourceChunk.IsCompressed,
|
|
SseType: sourceChunk.SseType,
|
|
SseMetadata: sourceChunk.SseMetadata,
|
|
}, nil
|
|
}
|
|
|
|
func (fs *FilerSink) replicateOneManifestChunk(ctx context.Context, sourceChunk *filer_pb.FileChunk, path string, sourceMtimeNs int64) (*filer_pb.FileChunk, error) {
|
|
// LookupFileId can transiently fail during a write burst (volume not yet
|
|
// registered). Retry like the data-chunk path does; the gate stops on
|
|
// supersession or, when that cannot be checked, after a few attempts.
|
|
var resolvedChunks []*filer_pb.FileChunk
|
|
resolveName := fmt.Sprintf("resolve manifest %s", sourceChunk.GetFileIdString())
|
|
missingGate := fs.newMissingSourceChunkGate(sourceChunk.GetFileIdString())
|
|
err := util.RetryUntil(resolveName, func() error {
|
|
rc, e := filer.ResolveOneChunkManifest(ctx, fs.filerSource.LookupFileId, sourceChunk, nil)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
resolvedChunks = rc
|
|
return nil
|
|
}, fs.manifestResolveRetryGate(path, sourceMtimeNs, sourceChunk.GetFileIdString(), missingGate))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve manifest %s: %w", sourceChunk.GetFileIdString(), missingGate.wrap(err))
|
|
}
|
|
|
|
replicatedResolvedChunks, err := fs.replicateChunks(ctx, resolvedChunks, path, sourceMtimeNs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("replicate manifest data chunks %s: %w", sourceChunk.GetFileIdString(), err)
|
|
}
|
|
|
|
manifestDataChunks := make([]*filer_pb.FileChunk, len(replicatedResolvedChunks))
|
|
for i, chunk := range replicatedResolvedChunks {
|
|
copied := *chunk
|
|
manifestDataChunks[i] = &copied
|
|
}
|
|
filer_pb.BeforeEntrySerialization(manifestDataChunks)
|
|
manifestData, err := proto.Marshal(&filer_pb.FileChunkManifest{
|
|
Chunks: manifestDataChunks,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal manifest %s: %w", sourceChunk.GetFileIdString(), err)
|
|
}
|
|
|
|
manifestFileId, err := fs.uploadManifestChunk(path, sourceMtimeNs, sourceChunk.GetFileIdString(), manifestData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &filer_pb.FileChunk{
|
|
FileId: manifestFileId,
|
|
Offset: sourceChunk.Offset,
|
|
Size: sourceChunk.Size,
|
|
ModifiedTsNs: sourceChunk.ModifiedTsNs,
|
|
ETag: sourceChunk.ETag,
|
|
SourceFileId: sourceChunk.GetFileIdString(),
|
|
IsChunkManifest: true,
|
|
}, nil
|
|
}
|
|
|
|
// maxUnverifiableResolveAttempts bounds manifest-resolve retries when
|
|
// supersession cannot be checked for the target path.
|
|
const maxUnverifiableResolveAttempts = 3
|
|
|
|
// manifestResolveRetryGate decides whether a failing manifest resolve keeps
|
|
// retrying: stop when the source superseded the replayed version (the caller
|
|
// skips it as lossless), stop once the source has been unable to produce the
|
|
// manifest for the whole grace period, stop immediately on non-transient errors
|
|
// (e.g. corrupt manifest data) so the configured metadata error policy applies,
|
|
// and stop after a few attempts when supersession cannot be checked at all (incremental
|
|
// dated target keys don't map back to a source path) — propagating lets
|
|
// filer.backup decide with the event's real source key instead of spinning
|
|
// here forever.
|
|
func (fs *FilerSink) manifestResolveRetryGate(path string, sourceMtimeNs int64, chunkName string, missingGate *missingSourceChunkGate) func(error) bool {
|
|
_, canCheckSupersession := fs.targetPathToSourcePath(path)
|
|
attempts := 0
|
|
return func(resolveErr error) (shouldContinue bool) {
|
|
if fs.hasSourceNewerVersion(path, sourceMtimeNs) {
|
|
glog.V(1).Infof("skip retrying stale source manifest %s for %s: %v", chunkName, path, resolveErr)
|
|
return false
|
|
}
|
|
if missingGate.isPermanent(resolveErr) {
|
|
glog.Errorf("source has not had manifest %s for %s in %v, giving up on it: %v",
|
|
chunkName, path, missingSourceChunkGrace, resolveErr)
|
|
return false
|
|
}
|
|
if !isTransientResolveError(resolveErr) {
|
|
glog.V(0).Infof("resolve manifest %s for %s: non-transient error, propagating: %v", chunkName, path, resolveErr)
|
|
return false
|
|
}
|
|
attempts++
|
|
if !canCheckSupersession && attempts >= maxUnverifiableResolveAttempts {
|
|
glog.V(0).Infof("resolve manifest %s for %s: supersession unverifiable, propagating after %d attempts: %v", chunkName, path, attempts, resolveErr)
|
|
return false
|
|
}
|
|
glog.V(0).Infof("resolve manifest %s for %s: %v", chunkName, path, resolveErr)
|
|
return true
|
|
}
|
|
}
|
|
|
|
// isTransientResolveError reports whether a manifest resolve failure belongs to
|
|
// the classes that clear on their own during a write burst — volume-lookup
|
|
// races and network interruptions. Anything else (corrupt manifest data, bad
|
|
// file ids) is permanent and must propagate rather than retry forever.
|
|
func isTransientResolveError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
if isRetryableNetworkError(err) {
|
|
return true
|
|
}
|
|
errStr := err.Error()
|
|
return strings.Contains(errStr, "LookupFileId") ||
|
|
(strings.Contains(errStr, "volume id") && strings.Contains(errStr, "not found"))
|
|
}
|
|
|
|
func (fs *FilerSink) uploadManifestChunk(path string, sourceMtimeNs int64, sourceFileId string, manifestData []byte) (fileId string, err error) {
|
|
uploader, err := fs.getUploader()
|
|
if err != nil {
|
|
glog.V(0).Infof("upload manifest data %v: %v", sourceFileId, err)
|
|
return "", fmt.Errorf("upload manifest data: %w", err)
|
|
}
|
|
|
|
retryName := fmt.Sprintf("replicate manifest chunk %s", sourceFileId)
|
|
err = util.RetryUntil(retryName, func() error {
|
|
uploadOption := &operation.UploadOption{
|
|
Filename: "",
|
|
Cipher: false,
|
|
IsInputCompressed: false,
|
|
MimeType: "application/octet-stream",
|
|
PairMap: nil,
|
|
RetryForever: false,
|
|
}
|
|
if fs.writeChunkByFiler {
|
|
uploadOption.GenUploadUrl = operation.GenUploadUrlProxy(fs.address)
|
|
}
|
|
currentFileId, uploadResult, uploadErr, _ := uploader.UploadWithRetry(
|
|
fs,
|
|
&filer_pb.AssignVolumeRequest{
|
|
Count: 1,
|
|
Replication: fs.replication,
|
|
Collection: fs.collection,
|
|
TtlSec: fs.ttlSec,
|
|
DataCenter: fs.dataCenter,
|
|
DiskType: fs.diskType,
|
|
Path: path,
|
|
},
|
|
uploadOption,
|
|
bytes.NewReader(manifestData),
|
|
)
|
|
if uploadErr != nil {
|
|
return fmt.Errorf("upload manifest data: %w", uploadErr)
|
|
}
|
|
if uploadResult.Error != "" {
|
|
return fmt.Errorf("upload manifest result: %v", uploadResult.Error)
|
|
}
|
|
|
|
fileId = currentFileId
|
|
return nil
|
|
}, func(uploadErr error) (shouldContinue bool) {
|
|
if fs.hasSourceNewerVersion(path, sourceMtimeNs) {
|
|
glog.V(1).Infof("skip retrying stale source manifest %s for %s: %v", sourceFileId, path, uploadErr)
|
|
return false
|
|
}
|
|
glog.V(0).Infof("replicate manifest %s for %s: %v", sourceFileId, path, uploadErr)
|
|
return true
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return fileId, nil
|
|
}
|
|
|
|
func (fs *FilerSink) fetchAndWrite(sourceChunk *filer_pb.FileChunk, path string, sourceMtimeNs int64) (fileId string, err error) {
|
|
uploader, err := fs.getUploader()
|
|
if err != nil {
|
|
glog.V(0).Infof("upload source data %v: %v", sourceChunk.GetFileIdString(), err)
|
|
return "", fmt.Errorf("upload data: %w", err)
|
|
}
|
|
|
|
transferStatus := &ChunkTransferStatus{
|
|
ChunkTransferSnapshot: ChunkTransferSnapshot{
|
|
ChunkFileId: sourceChunk.GetFileIdString(),
|
|
Path: path,
|
|
Status: "downloading",
|
|
},
|
|
}
|
|
fs.activeTransfers.Store(sourceChunk.GetFileIdString(), transferStatus)
|
|
defer fs.activeTransfers.Delete(sourceChunk.GetFileIdString())
|
|
|
|
transientBackoff := time.Duration(0)
|
|
_, canCheckSupersession := fs.targetPathToSourcePath(path)
|
|
missingGate := fs.newMissingSourceChunkGate(sourceChunk.GetFileIdString())
|
|
var partialData []byte
|
|
var savedFilename string
|
|
var savedHeader http.Header
|
|
var savedSourceUrl string
|
|
retryName := fmt.Sprintf("replicate chunk %s", sourceChunk.GetFileIdString())
|
|
err = util.RetryUntil(retryName, func() error {
|
|
filename, header, resp, readErr := fs.filerSource.ReadPart(sourceChunk.GetFileIdString(), int64(len(partialData)))
|
|
if readErr != nil {
|
|
return fmt.Errorf("read part %s: %w", sourceChunk.GetFileIdString(), readErr)
|
|
}
|
|
defer util_http.CloseResponse(resp)
|
|
|
|
// Save metadata from first successful response
|
|
if len(partialData) == 0 {
|
|
savedFilename = filename
|
|
savedHeader = header
|
|
if resp.Request != nil && resp.Request.URL != nil {
|
|
savedSourceUrl = resp.Request.URL.String()
|
|
}
|
|
}
|
|
|
|
// Read the response body
|
|
data, readBodyErr := io.ReadAll(resp.Body)
|
|
if readBodyErr != nil {
|
|
// Keep whatever bytes we received before the error
|
|
partialData = append(partialData, data...)
|
|
return fmt.Errorf("read body: %w", readBodyErr)
|
|
}
|
|
|
|
// Combine with previously accumulated partial data
|
|
var fullData []byte
|
|
if len(partialData) > 0 {
|
|
fullData = append(partialData, data...)
|
|
glog.V(0).Infof("resumed reading %s, got %d + %d = %d bytes",
|
|
sourceChunk.GetFileIdString(), len(partialData), len(data), len(fullData))
|
|
partialData = nil
|
|
} else {
|
|
fullData = data
|
|
}
|
|
|
|
if err := validateReplicatedReadSize(sourceChunk, len(fullData)); err != nil {
|
|
return err
|
|
}
|
|
fs.sourceServed(sourceChunk.GetFileIdString())
|
|
|
|
transferStatus.mu.Lock()
|
|
transferStatus.BytesReceived = int64(len(fullData))
|
|
transferStatus.Status = "uploading"
|
|
transferStatus.mu.Unlock()
|
|
|
|
uploadOption := &operation.UploadOption{
|
|
Filename: savedFilename,
|
|
Cipher: false,
|
|
IsInputCompressed: "gzip" == savedHeader.Get("Content-Encoding"),
|
|
MimeType: savedHeader.Get("Content-Type"),
|
|
PairMap: nil,
|
|
RetryForever: false,
|
|
SourceUrl: savedSourceUrl,
|
|
}
|
|
if fs.writeChunkByFiler {
|
|
uploadOption.GenUploadUrl = operation.GenUploadUrlProxy(fs.address)
|
|
}
|
|
currentFileId, uploadResult, uploadErr, _ := uploader.UploadWithRetry(
|
|
fs,
|
|
&filer_pb.AssignVolumeRequest{
|
|
Count: 1,
|
|
Replication: fs.replication,
|
|
Collection: fs.collection,
|
|
TtlSec: fs.ttlSec,
|
|
DataCenter: fs.dataCenter,
|
|
DiskType: fs.diskType,
|
|
Path: path,
|
|
},
|
|
uploadOption,
|
|
util.NewBytesReader(fullData),
|
|
)
|
|
if uploadErr != nil {
|
|
return fmt.Errorf("upload data: %w", uploadErr)
|
|
}
|
|
if uploadResult.Error != "" {
|
|
return fmt.Errorf("upload result: %v", uploadResult.Error)
|
|
}
|
|
|
|
transientBackoff = 0
|
|
fileId = currentFileId
|
|
return nil
|
|
}, func(retryErr error) (shouldContinue bool) {
|
|
if errors.Is(retryErr, errChunkSizeMismatch) {
|
|
glog.V(0).Infof("permanent size mismatch replicating %s for %s: %v",
|
|
sourceChunk.GetFileIdString(), path, retryErr)
|
|
transferStatus.mu.Lock()
|
|
transferStatus.LastErr = retryErr.Error()
|
|
transferStatus.mu.Unlock()
|
|
return false
|
|
}
|
|
if fs.hasSourceNewerVersion(path, sourceMtimeNs) {
|
|
glog.V(1).Infof("skip retrying stale source %s for %s: %v", sourceChunk.GetFileIdString(), path, retryErr)
|
|
return false
|
|
}
|
|
transferStatus.mu.Lock()
|
|
transferStatus.LastErr = retryErr.Error()
|
|
transferStatus.mu.Unlock()
|
|
if isSourceChunkMissing(retryErr) && !canCheckSupersession {
|
|
glog.V(0).Infof("source does not have %s for %s, supersession unverifiable, propagating: %v",
|
|
sourceChunk.GetFileIdString(), path, retryErr)
|
|
return false
|
|
}
|
|
if missingGate.isPermanent(retryErr) {
|
|
glog.Errorf("source has not had %s for %s in %v, giving up on it: %v",
|
|
sourceChunk.GetFileIdString(), path, missingSourceChunkGrace, retryErr)
|
|
return false
|
|
}
|
|
if isRetryableNetworkError(retryErr) {
|
|
transientBackoff = nextTransientBackoff(transientBackoff)
|
|
transferStatus.mu.Lock()
|
|
transferStatus.BytesReceived = int64(len(partialData))
|
|
transferStatus.Status = fmt.Sprintf("waiting %v", transientBackoff)
|
|
transferStatus.mu.Unlock()
|
|
glog.V(0).Infof("connection interrupted while replicating %s for %s (%d bytes received so far), backing off %v: %v",
|
|
sourceChunk.GetFileIdString(), path, len(partialData), transientBackoff, retryErr)
|
|
time.Sleep(transientBackoff)
|
|
transferStatus.mu.Lock()
|
|
transferStatus.Status = "downloading"
|
|
transferStatus.mu.Unlock()
|
|
} else {
|
|
glog.V(0).Infof("replicate %s for %s: %v", sourceChunk.GetFileIdString(), path, retryErr)
|
|
}
|
|
return true
|
|
})
|
|
if err != nil {
|
|
return "", missingGate.wrap(err)
|
|
}
|
|
|
|
return fileId, nil
|
|
}
|
|
|
|
const maxTransientBackoff = 2 * time.Minute
|
|
|
|
// nextTransientBackoff escalates 10s -> doubling -> 2m cap so an overloaded
|
|
// destination can recover instead of being hammered by the RetryUntil loop.
|
|
func nextTransientBackoff(current time.Duration) time.Duration {
|
|
if current < 10*time.Second {
|
|
return 10 * time.Second
|
|
}
|
|
current *= 2
|
|
if current > maxTransientBackoff {
|
|
current = maxTransientBackoff
|
|
}
|
|
return current
|
|
}
|
|
|
|
func isEofError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
return errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF)
|
|
}
|
|
|
|
// isRetryableNetworkError reports whether err is a transient network failure worth
|
|
// a backoff-and-retry. A bare io.EOF counts here but not in util.IsTransientError:
|
|
// mid-transfer it means the chunk read was truncated, not that a stream ended
|
|
// cleanly.
|
|
func isRetryableNetworkError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
return isEofError(err) || util.IsTransientError(err)
|
|
}
|
|
|
|
// errChunkSizeMismatch is a permanent (non-retriable) replication failure.
|
|
var errChunkSizeMismatch = errors.New("chunk size mismatch")
|
|
|
|
func validateReplicatedReadSize(sourceChunk *filer_pb.FileChunk, readSize int) error {
|
|
if uint64(readSize) != sourceChunk.Size {
|
|
return fmt.Errorf("%w: read %s got %d bytes, source metadata says %d",
|
|
errChunkSizeMismatch, sourceChunk.GetFileIdString(),
|
|
readSize, sourceChunk.Size)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (fs *FilerSink) newMissingSourceChunkGate(fileId string) *missingSourceChunkGate {
|
|
return &missingSourceChunkGate{sink: fs, volumeId: filer.VolumeId(fileId)}
|
|
}
|
|
|
|
// sourceVolumeMissingSince returns when the sink first found volumeId
|
|
// unlocatable, recording now on the first sighting.
|
|
func (fs *FilerSink) sourceVolumeMissingSince(volumeId string) time.Time {
|
|
since, _ := fs.missingVolumes.LoadOrStore(volumeId, time.Now())
|
|
return since.(time.Time)
|
|
}
|
|
|
|
// sourceServed records a chunk the source did serve: the probe
|
|
// sourceStillServesChunks re-checks, and proof its volume is locatable again.
|
|
func (fs *FilerSink) sourceServed(fileId string) {
|
|
fs.lastServedFileId.Store(&fileId)
|
|
fs.missingVolumes.Delete(filer.VolumeId(fileId))
|
|
}
|
|
|
|
// sourceStillServesChunks reports whether the source cluster can still produce the
|
|
// last chunk it served. A volume with no locations reads the same whether it was
|
|
// vacuumed away or every replica is down, so before an entry is written off the
|
|
// sink re-reads a file id the source did serve: if that one cannot be produced
|
|
// either the source is in an outage, and skipping would drop live files wholesale.
|
|
// It is a read and not a lookup because a lookup only proves the master still has
|
|
// the topology, not that a volume server answers.
|
|
func (fs *FilerSink) sourceStillServesChunks() bool {
|
|
if fs.filerSource == nil {
|
|
return false
|
|
}
|
|
probe := fs.lastServedFileId.Load()
|
|
if probe == nil {
|
|
return false
|
|
}
|
|
_, _, resp, err := fs.filerSource.ReadPart(*probe, 0)
|
|
if err != nil {
|
|
glog.V(0).Infof("source cannot serve %s either, so it is not only the one chunk: %v", *probe, err)
|
|
return false
|
|
}
|
|
util_http.CloseResponse(resp)
|
|
return true
|
|
}
|
|
|
|
// hasSourceNewerVersion reports whether the source's current entry for targetPath
|
|
// has moved past sourceMtimeNs — gone, or a strictly-newer mtime — meaning the
|
|
// version being replayed is stale. The lookup runs regardless of sourceMtimeNs so
|
|
// a deleted source is detected even when the replayed mtime is epoch/unset.
|
|
func (fs *FilerSink) hasSourceNewerVersion(targetPath string, sourceMtimeNs int64) bool {
|
|
sourcePath, ok := fs.targetPathToSourcePath(targetPath)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return SourceSupersedes(context.Background(), fs.filerSource, sourcePath, sourceMtimeNs)
|
|
}
|
|
|
|
// SourceSupersedes reports whether the live source's entry at sourcePath has
|
|
// moved past the replayed version (mtimeNs) — deleted or strictly newer — so
|
|
// skipping it is lossless. Exported for filer.backup's non-filer-sink decision.
|
|
func SourceSupersedes(ctx context.Context, filerSource *source.FilerSource, sourcePath util.FullPath, mtimeNs int64) bool {
|
|
if filerSource == nil {
|
|
return false
|
|
}
|
|
sourceEntry, _, _, err := filer_pb.GetEntry(ctx, filerSource, sourcePath)
|
|
return sourceSupersedes(sourcePath, sourceEntry, err, mtimeNs)
|
|
}
|
|
|
|
// sourceSupersedes reports whether the replayed version (sourceMtimeNs) is stale:
|
|
// true if the source entry is gone or strictly newer, false on a same/older version
|
|
// or any non-"not found" lookup error (so a transient failure never skips a live
|
|
// file). GetEntry signals missing as ErrNotFound — possibly wrapped or a plain gRPC
|
|
// string, occasionally a nil entry — all treated as gone.
|
|
func sourceSupersedes(sourcePath util.FullPath, sourceEntry *filer_pb.Entry, lookupErr error, sourceMtimeNs int64) bool {
|
|
if lookupErr != nil {
|
|
if errors.Is(lookupErr, filer_pb.ErrNotFound) || strings.Contains(lookupErr.Error(), filer_pb.ErrNotFound.Error()) {
|
|
glog.V(1).Infof("source entry %s no longer exists: %v", sourcePath, lookupErr)
|
|
return true
|
|
}
|
|
glog.V(1).Infof("lookup source entry %s: %v", sourcePath, lookupErr)
|
|
return false
|
|
}
|
|
if sourceEntry == nil {
|
|
glog.V(1).Infof("source entry %s no longer exists", sourcePath)
|
|
return true
|
|
}
|
|
|
|
// source still holds the entry; only a strictly-newer mtime proves staleness,
|
|
// and only when there is a valid replayed mtime to compare against.
|
|
if sourceMtimeNs <= 0 {
|
|
return false
|
|
}
|
|
return getEntryMtimeNs(sourceEntry) > sourceMtimeNs
|
|
}
|
|
|
|
func (fs *FilerSink) targetPathToSourcePath(targetPath string) (util.FullPath, bool) {
|
|
if fs.filerSource == nil {
|
|
return "", false
|
|
}
|
|
|
|
// Incremental sink keys carry a date prefix (sinkDir/YYYY-MM-DD/relPath) that
|
|
// can't be reversed to a source path; report unmappable rather than build a path
|
|
// under a nonexistent dated dir, which would read as ErrNotFound and skip a live entry.
|
|
if fs.isIncremental {
|
|
return "", false
|
|
}
|
|
|
|
normalizePath := func(p string) string {
|
|
p = strings.TrimSuffix(p, "/")
|
|
if p == "" {
|
|
return "/"
|
|
}
|
|
return p
|
|
}
|
|
|
|
sourceRoot := normalizePath(fs.filerSource.Dir)
|
|
targetRoot := normalizePath(fs.dir)
|
|
targetPath = normalizePath(targetPath)
|
|
|
|
var relative string
|
|
switch {
|
|
case targetRoot == "/":
|
|
relative = strings.TrimPrefix(targetPath, "/")
|
|
case targetPath == targetRoot:
|
|
relative = ""
|
|
case strings.HasPrefix(targetPath, targetRoot+"/"):
|
|
relative = targetPath[len(targetRoot)+1:]
|
|
default:
|
|
return "", false
|
|
}
|
|
|
|
if relative == "" {
|
|
return util.FullPath(sourceRoot), true
|
|
}
|
|
return util.FullPath(sourceRoot).Child(relative), true
|
|
}
|
|
|
|
var _ = filer_pb.FilerClient(&FilerSink{})
|
|
|
|
func (fs *FilerSink) WithFilerClient(streamingMode bool, fn func(filer_pb.SeaweedFilerClient) error) error {
|
|
|
|
return pb.WithGrpcClient(context.Background(), streamingMode, fs.signature, func(grpcConnection *grpc.ClientConn) error {
|
|
client := filer_pb.NewSeaweedFilerClient(grpcConnection)
|
|
return fn(client)
|
|
}, fs.grpcAddress, false, fs.grpcDialOption)
|
|
|
|
}
|
|
|
|
func (fs *FilerSink) AdjustedUrl(location *filer_pb.Location) string {
|
|
return location.Url
|
|
}
|
|
|
|
func (fs *FilerSink) GetDataCenter() string {
|
|
return fs.dataCenter
|
|
}
|