Files
upstream-seaweedfs/weed/shell/command_fs_merge_volumes_test.go
upstream-mirror 89f5be005d
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
snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 03:23:42 +05:00

349 lines
13 KiB
Go

package shell
import (
"strings"
"sync"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
func newMergeCmd(limitMB uint64, vols ...*master_pb.VolumeInformationMessage) *commandFsMergeVolumes {
c := &commandFsMergeVolumes{
volumes: make(map[needle.VolumeId]*master_pb.VolumeInformationMessage),
volumeSizeLimit: limitMB * 1024 * 1024,
}
for _, v := range vols {
c.volumes[needle.VolumeId(v.Id)] = v
}
return c
}
// An explicit -fromVolumeId/-toVolumeId pair must be honored as given, even when
// the source is larger than the target. The heuristic planner only ever merges
// a smaller volume into a larger one, which used to make this request a silent
// no-op.
func TestCreateMergePlan_HonorsExplicitDirection(t *testing.T) {
larger := &master_pb.VolumeInformationMessage{Id: 87, Size: 7192590976}
smaller := &master_pb.VolumeInformationMessage{Id: 83, Size: 7088822248}
c := newMergeCmd(250000, larger, smaller)
plan, err := c.createMergePlan("*", []needle.VolumeId{83}, needle.VolumeId(87))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := plan.targets[needle.VolumeId(87)]; len(got) != 1 || got[0] != needle.VolumeId(83) {
t.Fatalf("expected 87->83, got plan=%v", plan.targets)
}
// The reverse direction keeps working too.
plan, err = c.createMergePlan("*", []needle.VolumeId{87}, needle.VolumeId(83))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := plan.targets[needle.VolumeId(83)]; len(got) != 1 || got[0] != needle.VolumeId(87) {
t.Fatalf("expected 83->87, got plan=%v", plan.targets)
}
}
// Merging into an empty target is valid (e.g. consolidating into a freshly
// vacuumed volume); only an empty source should be rejected.
func TestCreateMergePlan_DirectedAllowsEmptyTarget(t *testing.T) {
from := &master_pb.VolumeInformationMessage{Id: 87, Size: 100}
emptyTo := &master_pb.VolumeInformationMessage{Id: 83, Size: 0}
c := newMergeCmd(250000, from, emptyTo)
plan, err := c.createMergePlan("*", []needle.VolumeId{83}, needle.VolumeId(87))
if err != nil {
t.Fatalf("unexpected error merging into empty target: %v", err)
}
if got := plan.targets[needle.VolumeId(87)]; len(got) != 1 || got[0] != needle.VolumeId(83) {
t.Fatalf("expected 87->83, got plan=%v", plan.targets)
}
}
// The directed planner rejects a self-map on its own, not just via Do, since it
// is exercised directly.
func TestCreateMergePlan_DirectedRejectsSelfMap(t *testing.T) {
v := &master_pb.VolumeInformationMessage{Id: 87, Size: 100}
c := newMergeCmd(250000, v)
_, err := c.createMergePlan("*", []needle.VolumeId{87}, needle.VolumeId(87))
if err == nil || !strings.Contains(err.Error(), "no volume id changes") {
t.Fatalf("expected self-map rejection, got %v", err)
}
}
func TestCreateMergePlan_DirectedRejectsIneligible(t *testing.T) {
cases := []struct {
name string
from, to *master_pb.VolumeInformationMessage
coll string
wantErr string
}{
{
name: "readonly source",
from: &master_pb.VolumeInformationMessage{Id: 87, Size: 100, ReadOnly: true},
to: &master_pb.VolumeInformationMessage{Id: 83, Size: 100},
coll: "*",
wantErr: "volume 87 is readonly",
},
{
name: "readonly target",
from: &master_pb.VolumeInformationMessage{Id: 87, Size: 100},
to: &master_pb.VolumeInformationMessage{Id: 83, Size: 100, ReadOnly: true},
coll: "*",
wantErr: "volume 83 is readonly",
},
{
name: "empty source",
from: &master_pb.VolumeInformationMessage{Id: 87, Size: 0},
to: &master_pb.VolumeInformationMessage{Id: 83, Size: 100},
coll: "*",
wantErr: "volume 87 is empty",
},
{
name: "wrong collection",
from: &master_pb.VolumeInformationMessage{Id: 87, Size: 100, Collection: "other"},
to: &master_pb.VolumeInformationMessage{Id: 83, Size: 100, Collection: "other"},
coll: "wanted",
wantErr: "volume 87 is not in collection",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := newMergeCmd(250000, tc.from, tc.to)
_, err := c.createMergePlan(tc.coll, []needle.VolumeId{needle.VolumeId(tc.to.Id)}, needle.VolumeId(tc.from.Id))
if err == nil {
t.Fatalf("expected error %q, got nil", tc.wantErr)
}
if got := err.Error(); !strings.Contains(got, tc.wantErr) {
t.Fatalf("expected error containing %q, got %q", tc.wantErr, got)
}
})
}
}
// A source that fits into no single target distributes across several, each
// chunk going to the target with the most remaining capacity.
func TestCreateMergePlan_DistributesAcrossMultipleTargets(t *testing.T) {
mb := uint64(1024 * 1024)
from := &master_pb.VolumeInformationMessage{Id: 112, Size: 60 * mb}
to1 := &master_pb.VolumeInformationMessage{Id: 111, Size: 60 * mb}
to2 := &master_pb.VolumeInformationMessage{Id: 107, Size: 70 * mb}
c := newMergeCmd(100, from, to1, to2)
plan, err := c.createMergePlan("*", []needle.VolumeId{111, 107}, needle.VolumeId(112))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := plan.targets[needle.VolumeId(112)]; len(got) != 2 {
t.Fatalf("expected two targets, got %v", got)
}
// 111 has 40 MB free, 107 has 30 MB free. The first chunk goes to the
// emptier 111; allocations keep balancing remaining capacity after that.
vid, ok := plan.allocate(needle.VolumeId(112), 5*mb)
if !ok || vid != needle.VolumeId(111) {
t.Fatalf("expected first chunk on 111, got %v ok=%v", vid, ok)
}
vid, ok = plan.allocate(needle.VolumeId(112), 10*mb)
if !ok || vid != needle.VolumeId(111) {
t.Fatalf("expected second chunk on 111 (35 MB free vs 30), got %v ok=%v", vid, ok)
}
vid, ok = plan.allocate(needle.VolumeId(112), 5*mb)
if !ok || vid != needle.VolumeId(107) {
t.Fatalf("expected third chunk on 107 (30 MB free vs 25), got %v ok=%v", vid, ok)
}
// A chunk larger than any remaining capacity is refused.
if vid, ok = plan.allocate(needle.VolumeId(112), 45*mb); ok {
t.Fatalf("expected no room for oversized chunk, got %v", vid)
}
// Smaller chunks still fit afterwards.
if _, ok = plan.allocate(needle.VolumeId(112), 20*mb); !ok {
t.Fatal("expected room for 20 MB chunk")
}
// A failed move's release makes the reservation reusable.
vid2, ok := plan.allocate(needle.VolumeId(112), 20*mb)
if !ok {
t.Fatal("expected room for second 20 MB chunk")
}
plan.release(needle.VolumeId(112), vid2, 20*mb)
if vid3, ok := plan.allocate(needle.VolumeId(112), 20*mb); !ok || vid3 != vid2 {
t.Fatalf("released capacity must be reusable, got %v ok=%v", vid3, ok)
}
}
// The combined free capacity of all targets must cover the source's live data.
func TestCreateMergePlan_MultiTargetRejectsInsufficientCapacity(t *testing.T) {
mb := uint64(1024 * 1024)
from := &master_pb.VolumeInformationMessage{Id: 112, Size: 90 * mb}
to1 := &master_pb.VolumeInformationMessage{Id: 111, Size: 60 * mb}
to2 := &master_pb.VolumeInformationMessage{Id: 107, Size: 60 * mb}
c := newMergeCmd(100, from, to1, to2)
_, err := c.createMergePlan("*", []needle.VolumeId{111, 107}, needle.VolumeId(112))
if err == nil || !strings.Contains(err.Error(), "cannot merge into volumes") {
t.Fatalf("expected capacity rejection, got %v", err)
}
}
// Without -fromVolumeId, a -toVolumeId list restricts the heuristic planner's
// target candidates to the listed volumes.
func TestCreateMergePlan_TargetListRestrictsHeuristic(t *testing.T) {
mb := uint64(1024 * 1024)
big := &master_pb.VolumeInformationMessage{Id: 1, Size: 80 * mb}
mid := &master_pb.VolumeInformationMessage{Id: 2, Size: 50 * mb}
small := &master_pb.VolumeInformationMessage{Id: 3, Size: 10 * mb}
c := newMergeCmd(100, big, mid, small)
plan, err := c.createMergePlan("*", []needle.VolumeId{2}, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := plan.targets[needle.VolumeId(3)]; len(got) != 1 || got[0] != needle.VolumeId(2) {
t.Fatalf("expected 3->2, got %v", plan.targets)
}
if plan.isSource(needle.VolumeId(1)) {
t.Fatalf("volume 1 must not merge anywhere, got %v", plan.targets)
}
}
func TestParseTargetVolumeIds(t *testing.T) {
if ids, err := parseTargetVolumeIds(""); err != nil || ids != nil {
t.Fatalf("empty arg: got %v, %v", ids, err)
}
if ids, err := parseTargetVolumeIds("0"); err != nil || ids != nil {
t.Fatalf("zero arg: got %v, %v", ids, err)
}
ids, err := parseTargetVolumeIds("111, 107")
if err != nil || len(ids) != 2 || ids[0] != needle.VolumeId(111) || ids[1] != needle.VolumeId(107) {
t.Fatalf("list arg: got %v, %v", ids, err)
}
if _, err = parseTargetVolumeIds("111,111"); err == nil {
t.Fatal("expected duplicate rejection")
}
if _, err = parseTargetVolumeIds("111,x"); err == nil {
t.Fatal("expected invalid id rejection")
}
if _, err = parseTargetVolumeIds("4294967297"); err == nil {
t.Fatal("expected uint32 overflow rejection")
}
}
// A volume reporting more deleted bytes than it holds must read as empty.
func TestGetVolumeSize_ClampsDeletedOverSize(t *testing.T) {
c := newMergeCmd(250000)
overDeleted := &master_pb.VolumeInformationMessage{Id: 1, Size: 1000, DeletedByteCount: 4000}
if got := c.getVolumeSize(overDeleted); got != 0 {
t.Errorf("expected 0 for a volume with more deleted than stored, got %d", got)
}
healthy := &master_pb.VolumeInformationMessage{Id: 2, Size: 3000, DeletedByteCount: 1000}
if got := c.getVolumeSize(healthy); got != 2000 {
t.Errorf("expected 2000, got %d", got)
}
}
// Manifests on volumes in a foreign collection cannot reference plan volumes
// and must be skipped instead of downloaded; plan sources and unknown volumes
// must still be resolved.
func TestManifestMayReferencePlan(t *testing.T) {
src := &master_pb.VolumeInformationMessage{Id: 1, Size: 100, Collection: "x"}
same := &master_pb.VolumeInformationMessage{Id: 2, Size: 100, Collection: "x"}
foreign := &master_pb.VolumeInformationMessage{Id: 3, Size: 100, Collection: "y"}
c := newMergeCmd(250000, src, same, foreign)
plan := newMergePlan(c.volumeSizeLimit)
plan.targets[needle.VolumeId(1)] = []needle.VolumeId{2}
planCollections := map[string]bool{"x": true}
for vid, want := range map[needle.VolumeId]bool{
1: true, // plan source
2: true, // same collection
3: false, // foreign collection
9: true, // unknown volume, resolve conservatively
} {
if got := c.manifestMayReferencePlan(plan, planCollections, vid); got != want {
t.Fatalf("volume %d: got %v, want %v", vid, got, want)
}
}
}
// A source whose index still holds needles but that the traversal never saw is
// made entirely of orphan needles — the merge moves nothing, but the operator
// must be told the real cleanup is fsck.
func TestWarnUnreferencedSources(t *testing.T) {
vol := &master_pb.VolumeInformationMessage{Id: 203, FileCount: 18}
c := newMergeCmd(250000, vol)
plan := &mergePlan{targets: map[needle.VolumeId][]needle.VolumeId{
needle.VolumeId(203): {needle.VolumeId(187)},
}}
var sb strings.Builder
c.warnUnreferencedSources(&sb, plan, newSourceNeedleCounter(), "/buckets/test")
out := sb.String()
if !strings.Contains(out, "volume 203") || !strings.Contains(out, "orphan needles") {
t.Fatalf("expected orphan warning, got %q", out)
}
// Needles seen during traversal: no warning.
seen := newSourceNeedleCounter()
for i := 0; i < 7; i++ {
seen.record(needle.VolumeId(203))
}
sb.Reset()
c.warnUnreferencedSources(&sb, plan, seen, "/buckets/test")
if sb.Len() != 0 {
t.Fatalf("expected no warning when needles were seen, got %q", sb.String())
}
// Zero FileCount (truly empty volume): nothing to report.
emptyVol := &master_pb.VolumeInformationMessage{Id: 204}
c2 := newMergeCmd(250000, emptyVol)
plan2 := &mergePlan{targets: map[needle.VolumeId][]needle.VolumeId{
needle.VolumeId(204): {needle.VolumeId(187)},
}}
sb.Reset()
c2.warnUnreferencedSources(&sb, plan2, newSourceNeedleCounter(), "/buckets/test")
if sb.Len() != 0 {
t.Fatalf("expected no warning for empty volume, got %q", sb.String())
}
}
// Concurrent BFS workers increment the production counter through record — the
// count must stay correct under -race with source-heavy inputs.
func TestWarnUnreferencedSources_ConcurrentRecording(t *testing.T) {
vol := &master_pb.VolumeInformationMessage{Id: 203, FileCount: 1000}
c := newMergeCmd(250000, vol)
plan := &mergePlan{targets: map[needle.VolumeId][]needle.VolumeId{
needle.VolumeId(203): {needle.VolumeId(187)},
}}
seen := newSourceNeedleCounter()
var wg sync.WaitGroup
for w := 0; w < 5; w++ { // TraverseBfs worker count
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 200; i++ {
seen.record(needle.VolumeId(203))
}
}()
}
wg.Wait()
if got := seen.count(needle.VolumeId(203)); got != 1000 {
t.Fatalf("lost increments under concurrency: %d", got)
}
var sb strings.Builder
c.warnUnreferencedSources(&sb, plan, seen, "/buckets/test")
if sb.Len() != 0 {
t.Fatalf("expected no warning when all needles were seen, got %q", sb.String())
}
}