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
346 lines
13 KiB
Go
346 lines
13 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/replication/sink"
|
|
"github.com/seaweedfs/seaweedfs/weed/replication/source"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
util_http.InitGlobalHttpClient()
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// readUrlError starts a test HTTP server returning the given status code
|
|
// and returns the error produced by ReadUrlAsStream.
|
|
//
|
|
// The error format is defined in ReadUrlAsStream:
|
|
// https://github.com/seaweedfs/seaweedfs/blob/3a765df2ff90839acb9acf910b73513417fa84d1/weed/util/http/http_global_client_util.go#L353
|
|
func readUrlError(t *testing.T, statusCode int) error {
|
|
t.Helper()
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, http.StatusText(statusCode), statusCode)
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := util_http.ReadUrlAsStream(context.Background(),
|
|
server.URL+"/437,03f591a3a2b95e?readDeleted=true", "",
|
|
nil, false, true, 0, 1024, func(data []byte) {})
|
|
if err == nil {
|
|
t.Fatal("expected error from ReadUrlAsStream, got nil")
|
|
}
|
|
return err
|
|
}
|
|
|
|
func TestIsIgnorable404_WrappedErrNotFound(t *testing.T) {
|
|
readErr := readUrlError(t, http.StatusNotFound)
|
|
// genProcessFunction wraps sink errors with %w:
|
|
// https://github.com/seaweedfs/seaweedfs/blob/3a765df2ff90839acb9acf910b73513417fa84d1/weed/command/filer_sync.go#L496
|
|
genErr := fmt.Errorf("create entry1 : %w", readErr)
|
|
|
|
if !isIgnorable404(genErr) {
|
|
t.Errorf("expected ignorable, got not: %v", genErr)
|
|
}
|
|
}
|
|
|
|
func TestIsIgnorable404_BrokenUnwrapChain(t *testing.T) {
|
|
readErr := readUrlError(t, http.StatusNotFound)
|
|
// AWS SDK v1 wraps transport errors via awserr.New which uses origErr.Error()
|
|
// instead of %w, so errors.Is cannot unwrap through it:
|
|
// https://github.com/aws/aws-sdk-go/blob/v1.55.8/aws/corehandlers/handlers.go#L173
|
|
// https://github.com/aws/aws-sdk-go/blob/v1.55.8/aws/awserr/types.go#L15
|
|
awsSdkErr := fmt.Errorf("RequestError: send request failed\n"+
|
|
"caused by: Put \"https://s3.amazonaws.com/bucket/key\": %s", readErr.Error())
|
|
genErr := fmt.Errorf("create entry1 : %w", awsSdkErr)
|
|
|
|
if !isIgnorable404(genErr) {
|
|
t.Errorf("expected ignorable, got not: %v", genErr)
|
|
}
|
|
}
|
|
|
|
func TestIsIgnorable404_NonIgnorableError(t *testing.T) {
|
|
readErr := readUrlError(t, http.StatusForbidden)
|
|
genErr := fmt.Errorf("create entry1 : %w", readErr)
|
|
|
|
if isIgnorable404(genErr) {
|
|
t.Errorf("expected not ignorable, got ignorable: %v", genErr)
|
|
}
|
|
}
|
|
|
|
// Regression for the partial-landing swallow: transient volume-lookup races
|
|
// ("LookupFileId ... failed", "volume id N not found") raised while replicating
|
|
// a checkpoint write burst must NOT be classified as a genuine source 404.
|
|
// They previously matched isIgnorable404 by substring, so the event was treated
|
|
// as a deletion — the subscription offset advanced and the file (typically a
|
|
// large manifest-backed .pt) was never replicated, with no error logged. They
|
|
// are now propagated so the offset stays put and the event is reprocessed; only
|
|
// a genuinely-gone source (verified live by the filer sink) is ever skipped.
|
|
func TestIsIgnorable404_TransientLookupNotSwallowed(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
err error
|
|
}{
|
|
{
|
|
"lookup file id race",
|
|
fmt.Errorf("create entry1 : %w",
|
|
fmt.Errorf("replicate manifest data chunks 3,01abc: LookupFileId 3,01abc failed, err: context deadline exceeded")),
|
|
},
|
|
{
|
|
"volume id not found race",
|
|
fmt.Errorf("create entry1 : %w",
|
|
fmt.Errorf("replicate entry chunks /buckets/x/model.pt: copy 7,02def: read part 7,02def: volume id 7 not found")),
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if isIgnorable404(tc.err) {
|
|
t.Errorf("transient lookup race must not be ignorable (would swallow a live file): %v", tc.err)
|
|
}
|
|
if !isSourceLookupError(tc.err) {
|
|
t.Errorf("lookup race must classify as a source lookup error (resolved via the live source): %v", tc.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestErrorClassifiersNilSafe(t *testing.T) {
|
|
if isIgnorable404(nil) {
|
|
t.Error("isIgnorable404(nil) must be false")
|
|
}
|
|
if isSourceLookupError(nil) {
|
|
t.Error("isSourceLookupError(nil) must be false")
|
|
}
|
|
}
|
|
|
|
func TestIsSourceLookupError_NonLookupErrors(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
err error
|
|
}{
|
|
{"genuine S3 404", fmt.Errorf("upload part: 404 Not Found: not found")},
|
|
{"network error", fmt.Errorf("dial tcp 10.0.0.1:8080: connection refused")},
|
|
{"plain not found without volume id", fmt.Errorf("entry not found")},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if isSourceLookupError(tc.err) {
|
|
t.Errorf("must not classify as a source lookup error: %v", tc.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Legacy events carry an empty NewParentPath; the probe must fall back to
|
|
// resp.Directory instead of building "/<name>", which would read as "gone"
|
|
// and skip a live file.
|
|
func TestEventSupersessionProbe_PathDerivation(t *testing.T) {
|
|
entry := &filer_pb.Entry{
|
|
Name: "f",
|
|
Attributes: &filer_pb.FuseAttributes{Mtime: 123},
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
resp *filer_pb.SubscribeMetadataResponse
|
|
want string
|
|
}{
|
|
{
|
|
"legacy event without NewParentPath",
|
|
&filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/buckets/x",
|
|
EventNotification: &filer_pb.EventNotification{NewEntry: entry},
|
|
},
|
|
"/buckets/x/f",
|
|
},
|
|
{
|
|
"rename event with NewParentPath",
|
|
&filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/buckets/x",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
NewParentPath: "/buckets/y",
|
|
NewEntry: entry,
|
|
},
|
|
},
|
|
"/buckets/y/f",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
path, mtimeNs, ok := eventSupersessionProbe(tc.resp)
|
|
if !ok {
|
|
t.Fatal("probe must succeed when NewEntry is present")
|
|
}
|
|
if string(path) != tc.want {
|
|
t.Errorf("path = %q, want %q", path, tc.want)
|
|
}
|
|
if mtimeNs != 123*int64(1e9) {
|
|
t.Errorf("mtimeNs = %d, want %d", mtimeNs, 123*int64(1e9))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// When supersession cannot be proven, never skip — that would drop a live file.
|
|
func TestEventSourceSuperseded_Guards(t *testing.T) {
|
|
if eventSourceSuperseded(nil, nil) {
|
|
t.Error("nil response must not be skippable")
|
|
}
|
|
if eventSourceSuperseded(nil, &filer_pb.SubscribeMetadataResponse{
|
|
EventNotification: &filer_pb.EventNotification{},
|
|
}) {
|
|
t.Error("event without NewEntry must not be skippable")
|
|
}
|
|
if eventSourceSuperseded(nil, &filer_pb.SubscribeMetadataResponse{
|
|
EventNotification: &filer_pb.EventNotification{
|
|
NewParentPath: "/buckets/x",
|
|
NewEntry: &filer_pb.Entry{
|
|
Name: "model.pt",
|
|
Attributes: &filer_pb.FuseAttributes{Mtime: 1234567890},
|
|
},
|
|
},
|
|
}) {
|
|
t.Error("nil filerSource must not be skippable")
|
|
}
|
|
}
|
|
|
|
// stubSink is a minimal ReplicationSink used to exercise initialSnapshotTargetKey
|
|
// and backupCheckpointIds without standing up a real sink; the methods those
|
|
// read (GetName, IsIncremental, GetSinkToDirectory, GetDestinationIdentity)
|
|
// reflect the fields, the rest satisfy the interface.
|
|
type stubSink struct {
|
|
name string
|
|
dir string
|
|
destination string
|
|
isIncremental bool
|
|
}
|
|
|
|
func (s *stubSink) GetName() string { return s.name }
|
|
func (s *stubSink) Initialize(util.Configuration, string) error { return nil }
|
|
func (s *stubSink) DeleteEntry(string, bool, bool, []int32) error {
|
|
return nil
|
|
}
|
|
func (s *stubSink) CreateEntry(string, *filer_pb.Entry, []int32) error { return nil }
|
|
func (s *stubSink) UpdateEntry(string, *filer_pb.Entry, string, *filer_pb.Entry, bool, []int32) (bool, error) {
|
|
return false, nil
|
|
}
|
|
func (s *stubSink) GetSinkToDirectory() string { return s.dir }
|
|
func (s *stubSink) GetDestinationIdentity() string { return s.destination }
|
|
func (s *stubSink) SetSourceFiler(*source.FilerSource) {}
|
|
func (s *stubSink) IsIncremental() bool { return s.isIncremental }
|
|
|
|
var _ sink.ReplicationSink = (*stubSink)(nil)
|
|
|
|
func TestInitialSnapshotTargetKey(t *testing.T) {
|
|
// Mirror the non-incremental path of buildKey so a refactor of one without
|
|
// the other will fail this test.
|
|
mirror := &stubSink{name: "mirror", isIncremental: false}
|
|
got := initialSnapshotTargetKey(mirror, "/backup", "/data", util.FullPath("/data/sub/file.txt"), &filer_pb.Entry{})
|
|
if got != "/backup/sub/file.txt" {
|
|
t.Errorf("mirror sink: got %q, want %q", got, "/backup/sub/file.txt")
|
|
}
|
|
|
|
// Incremental sinks partition by entry mtime, so the seed must use the same
|
|
// YYYY-MM-DD prefix a replayed CreateEntry would produce. buildKey in
|
|
// filer_sync.go formats the date in local time, so compute the expected
|
|
// key the same way to keep the test timezone-independent.
|
|
inc := &stubSink{name: "inc", isIncremental: true}
|
|
mtime := int64(1704196800) // 2024-01-02T12:00:00 UTC — unambiguously Jan 2 in nearly all timezones
|
|
gotInc := initialSnapshotTargetKey(inc, "/backup", "/data", util.FullPath("/data/sub/file.txt"), &filer_pb.Entry{
|
|
Attributes: &filer_pb.FuseAttributes{Mtime: mtime},
|
|
})
|
|
wantInc := "/backup/" + time.Unix(mtime, 0).Format("2006-01-02") + "/sub/file.txt"
|
|
if gotInc != wantInc {
|
|
t.Errorf("incremental sink: got %q, want %q", gotInc, wantInc)
|
|
}
|
|
|
|
// Trailing-slash sourcePath still produces a clean relative key.
|
|
gotTrail := initialSnapshotTargetKey(mirror, "/backup", "/data/", util.FullPath("/data/file.txt"), &filer_pb.Entry{})
|
|
if gotTrail != "/backup/file.txt" {
|
|
t.Errorf("trailing-slash sourcePath: got %q, want %q", gotTrail, "/backup/file.txt")
|
|
}
|
|
|
|
// Edge cases CodeRabbit called out: sourceKey equal to sourcePath
|
|
// (non-trailing and trailing variants). Real TraverseBfs walks never emit
|
|
// the root itself, but the helper must not panic if something else does.
|
|
if got := initialSnapshotTargetKey(mirror, "/backup", "/data", util.FullPath("/data"), &filer_pb.Entry{}); got != "/backup" {
|
|
t.Errorf("sourceKey == sourcePath (no slash): got %q, want %q", got, "/backup")
|
|
}
|
|
if got := initialSnapshotTargetKey(mirror, "/backup", "/data/", util.FullPath("/data"), &filer_pb.Entry{}); got != "/backup" {
|
|
t.Errorf("sourceKey == sourcePath (trailing slash mismatch): got %q, want %q", got, "/backup")
|
|
}
|
|
}
|
|
|
|
// The scenario from the collision report: two backups to different S3
|
|
// endpoints/buckets that share the destination directory "/" must not share
|
|
// a checkpoint, or the stopped one resumes from the other's position and
|
|
// skips changes.
|
|
func TestBackupCheckpointIds_DistinctDestinations(t *testing.T) {
|
|
backupA := &stubSink{name: "s3", dir: "/", destination: "s3.us-west-004.backblazeb2.com|seaweed-backup-a|/"}
|
|
backupB := &stubSink{name: "s3", dir: "/", destination: "s3.us-east-005.backblazeb2.com|seaweed-backup-b|/"}
|
|
|
|
idA, legacyA := backupCheckpointIds("/", backupA)
|
|
idB, legacyB := backupCheckpointIds("/", backupB)
|
|
|
|
if idA == idB {
|
|
t.Errorf("backups to different destinations share checkpoint id %d", idA)
|
|
}
|
|
// Both historically hashed to the same key — that is the bug the
|
|
// destination-scoped key fixes, and the shared value both fall back to.
|
|
if legacyA != legacyB {
|
|
t.Errorf("legacy ids differ: %d vs %d", legacyA, legacyB)
|
|
}
|
|
}
|
|
|
|
// Two backups of different source paths to the same destination must not
|
|
// share a checkpoint either: each stream sees a different event subset, so a
|
|
// shared offset lets the faster one push the slower one past unseen events.
|
|
func TestBackupCheckpointIds_DistinctSourcePaths(t *testing.T) {
|
|
s := &stubSink{name: "s3", dir: "/", destination: "endpoint|bucket|/"}
|
|
idA, _ := backupCheckpointIds("/buckets/a", s)
|
|
idB, _ := backupCheckpointIds("/buckets/b", s)
|
|
if idA == idB {
|
|
t.Errorf("backups of different source paths share checkpoint id %d", idA)
|
|
}
|
|
}
|
|
|
|
// The fallback key must keep the exact historical formula
|
|
// hash(GetName() + GetSinkToDirectory()) truncated to int32, or existing
|
|
// backups lose their checkpoint on upgrade and replay from zero.
|
|
func TestBackupCheckpointIds_LegacyFormulaUnchanged(t *testing.T) {
|
|
s := &stubSink{name: "s3", dir: "/data", destination: "endpoint|bucket|/data"}
|
|
_, legacy := backupCheckpointIds("/", s)
|
|
if want := int32(util.HashStringToLong("s3" + "/data")); legacy != want {
|
|
t.Errorf("legacy id = %d, want historical formula value %d", legacy, want)
|
|
}
|
|
}
|
|
|
|
// The NUL joins keep the hash input injective: field values spelling out
|
|
// other fields' content must not concatenate to the same input.
|
|
func TestBackupCheckpointIds_NoAliasing(t *testing.T) {
|
|
idA, _ := backupCheckpointIds("/src", &stubSink{name: "s3", dir: "/", destination: "/d=>s3|/other"})
|
|
idB, _ := backupCheckpointIds("/src=>s3|/d", &stubSink{name: "s3", dir: "/", destination: "/other"})
|
|
if idA == idB {
|
|
t.Errorf("field content spelling a separator aliases checkpoint id %d", idA)
|
|
}
|
|
}
|
|
|
|
// Restarting the same configuration must derive the same key, or every
|
|
// restart would orphan its checkpoint.
|
|
func TestBackupCheckpointIds_Stable(t *testing.T) {
|
|
s := &stubSink{name: "s3", dir: "/", destination: "endpoint|bucket|/"}
|
|
id1, legacy1 := backupCheckpointIds("/buckets/a", s)
|
|
id2, legacy2 := backupCheckpointIds("/buckets/a", s)
|
|
if id1 != id2 || legacy1 != legacy2 {
|
|
t.Errorf("ids not stable: (%d,%d) vs (%d,%d)", id1, legacy1, id2, legacy2)
|
|
}
|
|
}
|