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
336 lines
12 KiB
Go
336 lines
12 KiB
Go
package ec_balance
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/topology"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
|
)
|
|
|
|
// In-process (no real cluster) tests of the detection/planning path against a
|
|
// topology shaped the way the master reports a multi-disk cluster: several
|
|
// same-type physical disks on a node collapse into a single DiskInfo, with each
|
|
// shard's real DiskId surviving only in the per-shard records (issue 9593). They
|
|
// run the master-wire-format -> ActiveTopology -> Detection path, then simulate
|
|
// executing the planned moves with the volume server's actual semantics
|
|
// (VolumeEcShardsDelete is node-wide) to assert no EC shard is ever lost. The
|
|
// real-cluster end-to-end equivalent lives in test/erasure_coding.
|
|
|
|
// nodeSpec describes one volume server: its rack and the volume's shards per
|
|
// physical disk (diskID -> shard ids).
|
|
type nodeSpec struct {
|
|
id string
|
|
rack string
|
|
disks map[uint32][]int
|
|
}
|
|
|
|
const integDisksPerNode = 6
|
|
|
|
// buildMasterTopology renders nodeSpecs into a *master_pb.TopologyInfo exactly as
|
|
// the master would: one DiskInfo per node keyed by disk type (""), carrying one
|
|
// EcShardInfo per (volume, physical disk) plus regular VolumeInfos so every
|
|
// physical disk is discoverable even when it holds no EC shards.
|
|
func buildMasterTopology(collection string, vid uint32, maxVolPerDisk int, specs []nodeSpec) *master_pb.TopologyInfo {
|
|
rackByID := map[string]*master_pb.RackInfo{}
|
|
var rackOrder []string
|
|
|
|
for _, spec := range specs {
|
|
var ecShards []*master_pb.VolumeEcShardInformationMessage
|
|
for diskID, shards := range spec.disks {
|
|
var bits erasure_coding.ShardBits
|
|
for _, s := range shards {
|
|
bits = bits.Set(erasure_coding.ShardId(s))
|
|
}
|
|
ecShards = append(ecShards, &master_pb.VolumeEcShardInformationMessage{
|
|
Id: vid,
|
|
Collection: collection,
|
|
DiskId: diskID,
|
|
EcIndexBits: uint32(bits),
|
|
})
|
|
}
|
|
// Expose all physical disks (incl. EC-empty ones) via regular volumes.
|
|
var volInfos []*master_pb.VolumeInformationMessage
|
|
for d := uint32(0); d < integDisksPerNode; d++ {
|
|
volInfos = append(volInfos, &master_pb.VolumeInformationMessage{Id: 90000 + d, DiskId: d})
|
|
}
|
|
dn := &master_pb.DataNodeInfo{
|
|
Id: spec.id,
|
|
DiskInfos: map[string]*master_pb.DiskInfo{
|
|
"": {
|
|
Type: "",
|
|
MaxVolumeCount: int64(maxVolPerDisk * integDisksPerNode),
|
|
VolumeCount: int64(integDisksPerNode),
|
|
EcShardInfos: ecShards,
|
|
VolumeInfos: volInfos,
|
|
},
|
|
},
|
|
}
|
|
r, ok := rackByID[spec.rack]
|
|
if !ok {
|
|
r = &master_pb.RackInfo{Id: spec.rack}
|
|
rackByID[spec.rack] = r
|
|
rackOrder = append(rackOrder, spec.rack)
|
|
}
|
|
r.DataNodeInfos = append(r.DataNodeInfos, dn)
|
|
}
|
|
|
|
dc := &master_pb.DataCenterInfo{Id: "dc1"}
|
|
for _, rk := range rackOrder {
|
|
dc.RackInfos = append(dc.RackInfos, rackByID[rk])
|
|
}
|
|
return &master_pb.TopologyInfo{Id: "integ", DataCenterInfos: []*master_pb.DataCenterInfo{dc}}
|
|
}
|
|
|
|
// runDetection wires the topology through a real ActiveTopology and runs Detection.
|
|
func runDetection(t *testing.T, topoInfo *master_pb.TopologyInfo, cfg *Config) []*types.TaskDetectionResult {
|
|
t.Helper()
|
|
at := topology.NewActiveTopology(0)
|
|
if err := at.UpdateTopology(topoInfo); err != nil {
|
|
t.Fatalf("UpdateTopology: %v", err)
|
|
}
|
|
results, _, err := Detection(context.Background(), nil, &types.ClusterInfo{ActiveTopology: at}, cfg, 0)
|
|
if err != nil {
|
|
t.Fatalf("Detection: %v", err)
|
|
}
|
|
return results
|
|
}
|
|
|
|
// shardModel is nodeID -> diskID -> set of shard ids for one volume.
|
|
type shardModel map[string]map[uint32]map[int]bool
|
|
|
|
func modelFromSpecs(specs []nodeSpec) shardModel {
|
|
m := make(shardModel)
|
|
for _, spec := range specs {
|
|
m[spec.id] = make(map[uint32]map[int]bool)
|
|
for diskID, shards := range spec.disks {
|
|
set := make(map[int]bool)
|
|
for _, s := range shards {
|
|
set[s] = true
|
|
}
|
|
m[spec.id][diskID] = set
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m shardModel) distinctShards() []int {
|
|
seen := map[int]bool{}
|
|
for _, disks := range m {
|
|
for _, set := range disks {
|
|
for s := range set {
|
|
seen[s] = true
|
|
}
|
|
}
|
|
}
|
|
out := make([]int, 0, len(seen))
|
|
for s := range seen {
|
|
out = append(out, s)
|
|
}
|
|
sort.Ints(out)
|
|
return out
|
|
}
|
|
|
|
// applyMovesRealistically replays planned moves with the volume server's actual
|
|
// behavior: a move copies the shard to the destination disk, then deletes it on
|
|
// the source node — and EC shard delete is node-wide (removes the shard from
|
|
// every disk on the source node). A dedup move (same node+disk) is delete-only.
|
|
func (m shardModel) apply(results []*types.TaskDetectionResult) {
|
|
nodeWideDelete := func(node string, shard int) {
|
|
for diskID := range m[node] {
|
|
delete(m[node][diskID], shard)
|
|
}
|
|
}
|
|
for _, r := range results {
|
|
p := r.TypedParams
|
|
if p == nil || len(p.Sources) == 0 || len(p.Targets) == 0 {
|
|
continue
|
|
}
|
|
src, dst := p.Sources[0], p.Targets[0]
|
|
if len(src.ShardIds) == 0 {
|
|
continue
|
|
}
|
|
shard := int(src.ShardIds[0])
|
|
dedup := src.Node == dst.Node && src.DiskId == dst.DiskId
|
|
if !dedup {
|
|
if m[dst.Node] == nil {
|
|
m[dst.Node] = make(map[uint32]map[int]bool)
|
|
}
|
|
if m[dst.Node][dst.DiskId] == nil {
|
|
m[dst.Node][dst.DiskId] = make(map[int]bool)
|
|
}
|
|
m[dst.Node][dst.DiskId][shard] = true
|
|
}
|
|
nodeWideDelete(src.Node, shard)
|
|
}
|
|
}
|
|
|
|
// TestMultiDiskBalanceNeverLosesShards is the core regression for
|
|
// issue 9593: a freshly-encoded volume on a 3-node, 6-disk-per-node cluster,
|
|
// balanced and then concentrated, must never lose a shard when the planned moves
|
|
// are executed with real node-wide-delete semantics.
|
|
func TestMultiDiskBalanceNeverLosesShards(t *testing.T) {
|
|
cfg := NewDefaultConfig()
|
|
cfg.ImbalanceThreshold = 0.0 // balance to even; surface any move the planner makes
|
|
|
|
cases := []struct {
|
|
name string
|
|
specs []nodeSpec
|
|
}{
|
|
{
|
|
// Healthy post-encode spread: 14 shards across 3 nodes (5/5/4), each
|
|
// node spreading its shards over distinct physical disks.
|
|
name: "balanced across disks",
|
|
specs: []nodeSpec{
|
|
{id: "n1", rack: "rack1", disks: map[uint32][]int{0: {0}, 1: {1}, 2: {2}, 3: {3}, 4: {4}}},
|
|
{id: "n2", rack: "rack1", disks: map[uint32][]int{0: {5}, 1: {6}, 2: {7}, 3: {8}, 4: {9}}},
|
|
{id: "n3", rack: "rack1", disks: map[uint32][]int{0: {10}, 1: {11}, 2: {12}, 3: {13}}},
|
|
},
|
|
},
|
|
{
|
|
// Worst case from the report: all 14 shards landed on one node's disks;
|
|
// the balancer must redistribute without losing any.
|
|
name: "concentrated on one node",
|
|
specs: []nodeSpec{
|
|
{id: "n1", rack: "rack1", disks: map[uint32][]int{0: {0, 1, 2}, 1: {3, 4, 5}, 2: {6, 7}, 3: {8, 9}, 4: {10, 11}, 5: {12, 13}}},
|
|
{id: "n2", rack: "rack2", disks: map[uint32][]int{}},
|
|
{id: "n3", rack: "rack3", disks: map[uint32][]int{}},
|
|
},
|
|
},
|
|
{
|
|
// Multi-rack healthy spread.
|
|
name: "spread across racks",
|
|
specs: []nodeSpec{
|
|
{id: "n1", rack: "rack1", disks: map[uint32][]int{0: {0, 1}, 1: {2, 3}, 2: {4}}},
|
|
{id: "n2", rack: "rack2", disks: map[uint32][]int{0: {5, 6}, 1: {7, 8}, 2: {9}}},
|
|
{id: "n3", rack: "rack3", disks: map[uint32][]int{0: {10, 11}, 1: {12, 13}}},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
topoInfo := buildMasterTopology("col1", 28, 50, tc.specs)
|
|
results := runDetection(t, topoInfo, cfg)
|
|
|
|
model := modelFromSpecs(tc.specs)
|
|
before := model.distinctShards()
|
|
|
|
for _, r := range results {
|
|
// No move may be a same-node cross-disk move (node-wide delete
|
|
// would erase the shard after a skipped copy).
|
|
p := r.TypedParams
|
|
s, d := p.Sources[0], p.Targets[0]
|
|
if s.Node == d.Node && s.DiskId != d.DiskId {
|
|
t.Errorf("unsafe same-node cross-disk move: vol %d shard %v on %s disk %d->%d", p.VolumeId, s.ShardIds, s.Node, s.DiskId, d.DiskId)
|
|
}
|
|
// Source disk must actually hold the shard being moved.
|
|
shard := int(s.ShardIds[0])
|
|
if !model[s.Node][s.DiskId][shard] && s.Node != d.Node {
|
|
t.Errorf("move sources vol %d shard %d from %s disk %d, which does not hold it", p.VolumeId, shard, s.Node, s.DiskId)
|
|
}
|
|
}
|
|
|
|
model.apply(results)
|
|
after := model.distinctShards()
|
|
|
|
if len(after) != len(before) {
|
|
t.Errorf("[%s] shard loss: had %v (%d), now %v (%d) after %d moves",
|
|
tc.name, before, len(before), after, len(after), len(results))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConcentratedVolumeSpreadsAcrossNodesAndDisks asserts the
|
|
// remediation actually happens: a one-node-concentrated volume is redistributed
|
|
// to the other racks, landing on multiple distinct destination disks.
|
|
func TestConcentratedVolumeSpreadsAcrossNodesAndDisks(t *testing.T) {
|
|
cfg := NewDefaultConfig()
|
|
cfg.ImbalanceThreshold = 0.0
|
|
|
|
specs := []nodeSpec{
|
|
{id: "n1", rack: "rack1", disks: map[uint32][]int{0: {0, 1, 2}, 1: {3, 4, 5}, 2: {6, 7}, 3: {8, 9}, 4: {10, 11}, 5: {12, 13}}},
|
|
{id: "n2", rack: "rack2", disks: map[uint32][]int{}},
|
|
{id: "n3", rack: "rack3", disks: map[uint32][]int{}},
|
|
}
|
|
results := runDetection(t, buildMasterTopology("col1", 28, 50, specs), cfg)
|
|
if len(results) == 0 {
|
|
t.Fatal("expected redistribution moves for a one-node-concentrated volume")
|
|
}
|
|
|
|
destNodes := map[string]bool{}
|
|
destDisksByNode := map[string]map[uint32]bool{}
|
|
for _, r := range results {
|
|
d := r.TypedParams.Targets[0]
|
|
if d.Node == "n1" {
|
|
continue
|
|
}
|
|
destNodes[d.Node] = true
|
|
if destDisksByNode[d.Node] == nil {
|
|
destDisksByNode[d.Node] = map[uint32]bool{}
|
|
}
|
|
destDisksByNode[d.Node][d.DiskId] = true
|
|
}
|
|
if len(destNodes) < 2 {
|
|
t.Errorf("shards spread to only %d destination nodes, want both other racks", len(destNodes))
|
|
}
|
|
for node, disks := range destDisksByNode {
|
|
if len(disks) < 2 {
|
|
t.Errorf("destination %s received shards on only %d disk(s); expected spread across disks: %v", node, len(disks), disks)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildBalancerTopologyNormalizesHddDiskType guards the disk-type filter:
|
|
// the master reports default-HDD disks under the empty-string key, so a config of
|
|
// "hdd" must match them (not filter everything out), while "ssd" must exclude them.
|
|
func TestBuildBalancerTopologyNormalizesHddDiskType(t *testing.T) {
|
|
specs := []nodeSpec{
|
|
{id: "n1", rack: "r1", disks: map[uint32][]int{0: {0, 1}}},
|
|
{id: "n2", rack: "r1", disks: map[uint32][]int{0: {2}}},
|
|
}
|
|
topoInfo := buildMasterTopology("c", 100, 50, specs)
|
|
|
|
if _, n, _ := buildBalancerTopology(topoInfo, &Config{DiskType: "hdd"}, nil); n != 2 {
|
|
t.Errorf("disk_type=hdd matched %d nodes on an all-HDD cluster, want 2 (hdd must map to the empty HDD key)", n)
|
|
}
|
|
if _, n, _ := buildBalancerTopology(topoInfo, &Config{DiskType: ""}, nil); n != 2 {
|
|
t.Errorf("disk_type=empty matched %d nodes, want 2 (all)", n)
|
|
}
|
|
if _, n, _ := buildBalancerTopology(topoInfo, &Config{DiskType: "ssd"}, nil); n != 0 {
|
|
t.Errorf("disk_type=ssd matched %d nodes on an all-HDD cluster, want 0", n)
|
|
}
|
|
}
|
|
|
|
// TestResolveReplicaPlacementFallsBackToMasterDefault verifies the worker mirrors
|
|
// the shell: explicit config wins, otherwise the master's default replication is
|
|
// the fallback, and an empty or zero-replication value means no constraint.
|
|
func TestResolveReplicaPlacementFallsBackToMasterDefault(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
configRP string
|
|
defaultRP string
|
|
wantApplied bool
|
|
}{
|
|
{"explicit config used", "010", "", true},
|
|
{"explicit config wins over default", "010", "100", true},
|
|
{"falls back to master default", "", "010", true},
|
|
{"zero master default = no constraint", "", "000", false},
|
|
{"empty everywhere = no constraint", "", "", false},
|
|
{"invalid value ignored", "", "nonsense", false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rp := resolveReplicaPlacement(&Config{ReplicaPlacement: tc.configRP},
|
|
&types.ClusterInfo{DefaultReplicaPlacement: tc.defaultRP})
|
|
if (rp != nil) != tc.wantApplied {
|
|
t.Errorf("config=%q default=%q: applied=%v, want %v", tc.configRP, tc.defaultRP, rp != nil, tc.wantApplied)
|
|
}
|
|
})
|
|
}
|
|
}
|