snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-16 03:23:42 +05:00
commit 89f5be005d
3997 changed files with 961970 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
package topology
import (
"context"
"time"
"github.com/seaweedfs/seaweedfs/weed/operation"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"google.golang.org/grpc"
)
// Cap the RPC so a hung volume server can't strand the layout's
// growRequest flag and block all future automatic growth.
const allocateVolumeTimeout = 1 * time.Minute
type AllocateVolumeResult struct {
Error string
}
func AllocateVolume(dn *DataNode, grpcDialOption grpc.DialOption, vid needle.VolumeId, option *VolumeGrowOption) error {
return operation.WithVolumeServerClient(false, dn.ServerAddress(), grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
ctx, cancel := context.WithTimeout(context.Background(), allocateVolumeTimeout)
defer cancel()
_, allocateErr := client.AllocateVolume(ctx, &volume_server_pb.AllocateVolumeRequest{
VolumeId: uint32(vid),
Collection: option.Collection,
Replication: option.ReplicaPlacement.String(),
Ttl: option.Ttl.String(),
Preallocate: option.Preallocate,
MemoryMapMaxSizeMb: option.MemoryMapMaxSizeMb,
DiskType: string(option.DiskType),
Version: option.Version,
})
return allocateErr
})
}
func DeleteVolume(dn *DataNode, grpcDialOption grpc.DialOption, vid needle.VolumeId) error {
return operation.WithVolumeServerClient(false, dn.ServerAddress(), grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
ctx, cancel := context.WithTimeout(context.Background(), allocateVolumeTimeout)
defer cancel()
_, allocateErr := client.VolumeDelete(ctx, &volume_server_pb.VolumeDeleteRequest{
VolumeId: uint32(vid),
})
return allocateErr
})
}

View File

@@ -0,0 +1,52 @@
package balancer
import "math"
// zeroVolumeDensityWeight is the ratio given to a server holding no data, so it
// still ranks below any loaded server (which has ratio >= 1) but is distinguished
// from a server with unknown capacity.
const zeroVolumeDensityWeight = 0.5
// UsedVolumeEquivalents converts the bytes a server holds into whole-volume
// equivalents (volumeBytes / volumeSizeLimit). Returns 0 when the limit is unset.
func UsedVolumeEquivalents(volumeBytes, volumeSizeLimitBytes uint64) uint64 {
if volumeSizeLimitBytes == 0 {
return 0
}
return volumeBytes / volumeSizeLimitBytes
}
// VolumeDensity is the bytes-aware capacity view balancing ranks servers by:
// used = volumeBytes / volumeSizeLimit, capacity = maxVolumeCount - used. Using
// real data (not just volume count) means an over-configured maxVolumeCount can't
// make a byte-full server look empty. capacity may be <= 0 for a server already
// past its configured slots.
func VolumeDensity(maxVolumeCount int64, volumeBytes, volumeSizeLimitBytes uint64) (capacity float64, usedVolumes uint64) {
usedVolumes = UsedVolumeEquivalents(volumeBytes, volumeSizeLimitBytes)
return float64(maxVolumeCount - int64(usedVolumes)), usedVolumes
}
// DensityRatio is a server's load: usedVolumes / capacity, higher = fuller. An
// empty server gets a small positive ratio so it sorts below loaded servers.
// A server with no free capacity (capacity <= 0: full, or over its configured
// slots so capacity is negative) ranks as the fullest (+Inf), so ascending
// consumers treat it as a move source and never as the emptiest target.
func DensityRatio(capacity float64, usedVolumes uint64) float64 {
if capacity <= 0 {
return math.Inf(1)
}
if usedVolumes == 0 {
return zeroVolumeDensityWeight / capacity
}
return float64(usedVolumes) / capacity
}
// DensityNextRatio is a server's load after one more volume lands on it:
// (usedVolumes+1) / capacity. Used to test whether a move would overshoot. A
// server with no free capacity (capacity <= 0) ranks as the fullest (+Inf).
func DensityNextRatio(capacity float64, usedVolumes uint64) float64 {
if capacity <= 0 {
return math.Inf(1)
}
return float64(usedVolumes+1) / capacity
}

View File

@@ -0,0 +1,81 @@
package balancer
import (
"math"
"sort"
"testing"
)
func TestVolumeDensity(t *testing.T) {
const gb = uint64(1) << 30
// 20 full-ish volumes (~1900 GB) on a disk with a 30 GB limit and Max 1000.
cap, used := VolumeDensity(1000, 1900*gb, 30*gb)
if used != 63 { // 1900/30 = 63.33 -> 63
t.Errorf("usedVolumes = %d, want 63", used)
}
if cap != float64(1000-63) {
t.Errorf("capacity = %v, want %v", cap, float64(1000-63))
}
// Unset limit -> 0 used, capacity = maxVolumeCount.
if c, u := VolumeDensity(10, 1000*gb, 0); u != 0 || c != 10 {
t.Errorf("unset limit: got cap=%v used=%d, want 10/0", c, u)
}
}
func TestDensityRatio(t *testing.T) {
// Empty server: small positive ratio (below any loaded server).
empty := DensityRatio(1000, 0)
loaded := DensityRatio(1000, 1)
if !(empty > 0 && empty < loaded) {
t.Errorf("empty ratio %v should be >0 and < loaded %v", empty, loaded)
}
// Loaded: used/capacity.
if got := DensityRatio(3, 7); got != 7.0/3.0 {
t.Errorf("DensityRatio(3,7) = %v, want %v", got, 7.0/3.0)
}
// No free capacity (full) or over the slot limit (negative capacity) -> fullest.
if got := DensityRatio(0, 5); !math.IsInf(got, 1) {
t.Errorf("DensityRatio(0,5) = %v, want +Inf", got)
}
if got := DensityRatio(-4, 20); !math.IsInf(got, 1) {
t.Errorf("DensityRatio(-4,20) = %v, want +Inf (overfull ranks fullest, not negative)", got)
}
}
func TestDensityNextRatio(t *testing.T) {
if got := DensityNextRatio(3, 7); got != 8.0/3.0 {
t.Errorf("DensityNextRatio(3,7) = %v, want %v", got, 8.0/3.0)
}
if got := DensityNextRatio(0, 7); !math.IsInf(got, 1) {
t.Errorf("DensityNextRatio(0,7) = %v, want +Inf", got)
}
if got := DensityNextRatio(-1, 7); !math.IsInf(got, 1) {
t.Errorf("DensityNextRatio(-1,7) = %v, want +Inf", got)
}
}
// An overfull server (negative capacity) must sort as the fullest, not the
// emptiest, when consumers rank ascending by DensityRatio.
func TestDensityRatio_OverfullSortsFullest(t *testing.T) {
// empty (cap 100, used 0), half (cap 5, used 5), overfull (cap -3, used 13).
type server struct {
name string
capacity float64
usedVolumes uint64
}
servers := []server{
{"overfull", -3, 13},
{"empty", 100, 0},
{"half", 5, 5},
}
sort.Slice(servers, func(i, j int) bool {
return DensityRatio(servers[i].capacity, servers[i].usedVolumes) <
DensityRatio(servers[j].capacity, servers[j].usedVolumes)
})
if servers[0].name != "empty" {
t.Errorf("emptiest (ascending first) = %q, want empty", servers[0].name)
}
if servers[len(servers)-1].name != "overfull" {
t.Errorf("fullest (ascending last) = %q, want overfull", servers[len(servers)-1].name)
}
}

View File

@@ -0,0 +1,33 @@
// Package balancer holds balancing policy shared by the shell volume.balance
// command and the maintenance balance worker so the two implementations do not
// drift. It is dependency-light and takes plain values, so both the raw
// master_pb world (shell) and the ActiveTopology world (worker) can call it.
package balancer
// DefaultMaxDiskUsagePercent is the default physical-disk high-water mark for
// balancing: a move target whose disk is at or above this used percentage is
// skipped. It guards against an over-configured maxVolumeCount making a
// physically full server look under-utilized by slot count.
const DefaultMaxDiskUsagePercent = 90
// DiskTooFullAfter reports whether landing incomingBytes on a disk would push its
// physical used space to or above maxUsagePercent of totalBytes. It judges the
// disk against its own total, so heterogeneous disk sizes are handled correctly.
//
// It returns false (no opinion) when the gate is disabled (maxUsagePercent <= 0
// or >= 100) or the disk reports no capacity (totalBytes == 0), so callers fall
// back to slot-only behavior for servers that do not report disk bytes.
//
// Pass incomingBytes = 0 to test current fullness, or a volume's worth of bytes
// to test fullness after the move.
func DiskTooFullAfter(totalBytes, freeBytes, incomingBytes uint64, maxUsagePercent int) bool {
if maxUsagePercent <= 0 || maxUsagePercent >= 100 || totalBytes == 0 {
return false
}
var used uint64
if totalBytes > freeBytes {
used = totalBytes - freeBytes
}
usedAfter := float64(used) + float64(incomingBytes)
return usedAfter*100 >= float64(totalBytes)*float64(maxUsagePercent)
}

View File

@@ -0,0 +1,31 @@
package balancer
import "testing"
func TestDiskTooFullAfter(t *testing.T) {
const gb = uint64(1) << 30
tests := []struct {
name string
total, free, incoming uint64
pct int
want bool
}{
{"disabled at 0", 1000 * gb, 40 * gb, 0, 0, false},
{"disabled at 100", 1000 * gb, 40 * gb, 0, 100, false},
{"not reported (total 0)", 0, 0, 0, 90, false},
{"below mark", 1000 * gb, 300 * gb, 0, 90, false}, // 70% used
{"at mark", 1000 * gb, 100 * gb, 0, 90, true}, // exactly 90% used
{"above mark", 1000 * gb, 40 * gb, 0, 90, true}, // 96% used
{"below but crosses with incoming", 1000 * gb, 120 * gb, 30 * gb, 90, true}, // 88% -> 91%
{"below and stays below with incoming", 1000 * gb, 300 * gb, 30 * gb, 90, false},
{"free exceeds total (defensive)", 1000 * gb, 2000 * gb, 0, 90, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := DiskTooFullAfter(tc.total, tc.free, tc.incoming, tc.pct); got != tc.want {
t.Errorf("DiskTooFullAfter(%d,%d,%d,%d) = %v, want %v",
tc.total, tc.free, tc.incoming, tc.pct, got, tc.want)
}
})
}
}

View File

@@ -0,0 +1,176 @@
package balancer
import (
"slices"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
)
// Location identifies where a volume replica lives, at the granularity replica
// placement reasons about: data center, rack, data node, and physical host
// (machine). Servers sharing a host are one fault domain. It is the shared shape
// the shell (from master_pb) and the worker (from ActiveTopology) both adapt to.
type Location struct {
DataCenter string
Rack string
NodeID string
Host string
}
// HostFromAddress returns the physical machine (host/IP) of a server address,
// falling back to the node id (== ip:port in the common case) when no address is
// known.
func HostFromAddress(address, nodeID string) string {
if address == "" {
address = nodeID
}
return pb.ServerAddress(address).ToHost()
}
type rackKey struct {
DataCenter string
Rack string
}
type nodeKey struct {
DataCenter string
Rack string
NodeID string
}
// IsGoodMove reports whether moving a volume from sourceNodeID onto target keeps
// the volume's replica placement policy satisfied, given the current replica
// locations. It refuses moving onto the source itself, onto a node that already
// holds a replica, and onto a host that already holds another replica (best-effort
// machine anti-affinity), then defers to SatisfyReplicaPlacement.
func IsGoodMove(rp *super_block.ReplicaPlacement, existingReplicas []Location, sourceNodeID string, target Location) bool {
if rp == nil || !rp.HasReplication() {
return true // no replication constraint
}
// Never move onto the source node itself.
if target.NodeID == sourceNodeID {
return false
}
// Build the replica set after the move: remove the one replica being moved off
// the source, keeping any others (an over-replicated node may hold more than
// one replica of this volume, and only one is moving).
afterMove := make([]Location, 0, len(existingReplicas))
sourceFound := false
for _, r := range existingReplicas {
if r.NodeID == sourceNodeID && !sourceFound {
sourceFound = true
} else {
afterMove = append(afterMove, r)
}
}
if !sourceFound {
// Source not in the replica list — cluster state may be inconsistent.
// Treat as unsafe to avoid an incorrect placement decision.
return false
}
// Best-effort machine anti-affinity: don't move a replica onto a host that
// already holds another replica of this volume, so a single machine failure
// can't take out two replicas.
if target.Host != "" {
for _, r := range afterMove {
if r.Host == target.Host {
return false
}
}
}
return SatisfyReplicaPlacement(rp, afterMove, target)
}
// SatisfyReplicaPlacement reports whether placing a replica at target is
// consistent with the replication policy given the existing replicas. It is the
// shared implementation for the shell (volume.balance / fix.replication /
// tier.move) and the maintenance balance worker.
func SatisfyReplicaPlacement(rp *super_block.ReplicaPlacement, replicas []Location, target Location) bool {
existingDCs, _, existingNodes := countReplicas(replicas)
targetNK := nodeKey{DataCenter: target.DataCenter, Rack: target.Rack, NodeID: target.NodeID}
if _, found := existingNodes[targetNK]; found {
// avoid a duplicated volume on the same data node
return false
}
primaryDCs, _ := findTopDCKeys(existingDCs)
// ensure the data center count is within limit
if _, found := existingDCs[target.DataCenter]; !found {
// different from existing dcs
return len(existingDCs) < rp.DiffDataCenterCount+1
}
// same as one of the existing data centers
if !slices.Contains(primaryDCs, target.DataCenter) {
return false
}
// on a primary dc — check racks within this DC
primaryDcRacks := make(map[rackKey]int)
for _, r := range replicas {
if r.DataCenter != target.DataCenter {
continue
}
primaryDcRacks[rackKey{DataCenter: r.DataCenter, Rack: r.Rack}]++
}
targetRK := rackKey{DataCenter: target.DataCenter, Rack: target.Rack}
primaryRacks, _ := findTopRackKeys(primaryDcRacks)
sameRackCount := primaryDcRacks[targetRK]
if _, found := primaryDcRacks[targetRK]; !found {
// different from existing racks
return len(primaryDcRacks) < rp.DiffRackCount+1
}
// same as one of the existing racks
if !slices.Contains(primaryRacks, targetRK) {
return false
}
// on the primary rack — check the same-rack count
return sameRackCount < rp.SameRackCount+1
}
func countReplicas(replicas []Location) (dcCounts map[string]int, rackCounts map[rackKey]int, nodeCounts map[nodeKey]int) {
dcCounts = make(map[string]int)
rackCounts = make(map[rackKey]int)
nodeCounts = make(map[nodeKey]int)
for _, r := range replicas {
dcCounts[r.DataCenter]++
rackCounts[rackKey{DataCenter: r.DataCenter, Rack: r.Rack}]++
nodeCounts[nodeKey{DataCenter: r.DataCenter, Rack: r.Rack, NodeID: r.NodeID}]++
}
return
}
func findTopDCKeys(m map[string]int) (topKeys []string, max int) {
for k, c := range m {
if max < c {
topKeys = topKeys[:0]
topKeys = append(topKeys, k)
max = c
} else if max == c {
topKeys = append(topKeys, k)
}
}
return
}
func findTopRackKeys(m map[rackKey]int) (topKeys []rackKey, max int) {
for k, c := range m {
if max < c {
topKeys = topKeys[:0]
topKeys = append(topKeys, k)
max = c
} else if max == c {
topKeys = append(topKeys, k)
}
}
return
}

View File

@@ -0,0 +1,36 @@
package balancer
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
)
func loc(dc, rack, node string) Location {
return Location{DataCenter: dc, Rack: rack, NodeID: node}
}
// When a node abnormally holds more than one replica of a volume, moving one of
// them off must keep the other in the after-move set, so the placement check
// still sees the rack's true occupancy.
func TestIsGoodMove_OverReplicatedSourceKeepsOtherReplica(t *testing.T) {
rp, err := super_block.NewReplicaPlacementFromString("001") // up to 2 copies in a rack
if err != nil {
t.Fatal(err)
}
// n1 abnormally holds two replicas; n2 holds a third — all in rack r1.
existing := []Location{loc("dc1", "r1", "n1"), loc("dc1", "r1", "n1"), loc("dc1", "r1", "n2")}
// Moving ONE replica off n1 onto a new node n3 in r1 must be rejected: r1 would
// still hold two replicas (the retained n1 and n2), at the SameRackCount limit.
if IsGoodMove(rp, existing, "n1", loc("dc1", "r1", "n3")) {
t.Error("expected reject: moving one replica off an over-replicated node must keep its other replica in the rack count")
}
// The normal case is unaffected: two replicas correctly in r1, move one to a
// third node in r1 — still within the SameRackCount=1 (two-per-rack) limit.
normal := []Location{loc("dc1", "r1", "n1"), loc("dc1", "r1", "n2")}
if !IsGoodMove(rp, normal, "n1", loc("dc1", "r1", "n3")) {
t.Error("expected allow: a normal move within the rack limit")
}
}

View File

@@ -0,0 +1,161 @@
package balancer
import (
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
)
// Replica pairs a replica's placement location with the volume attributes
// replica-selection decisions need. Like Location for placement checks, it is
// the shared shape the shell (from master_pb) and workers adapt to. Selection
// functions return an index into the given slice (-1 when nothing qualifies),
// so callers can map the choice back to their own richer replica type.
type Replica struct {
Location Location
Size uint64
ModifiedAtSecond int64
CompactRevision uint32
ReadOnly bool
}
// SatisfyReplicaCurrentLocation reports whether the existing replicas already
// span the failure domains the policy requires (enough data centers, enough
// racks, and the same-rack count somewhere). A volume whose replica count is
// right but whose spread falls short still needs another replica.
func SatisfyReplicaCurrentLocation(rp *super_block.ReplicaPlacement, replicas []Location) bool {
dcCounts, rackCounts, _ := countReplicas(replicas)
if rp.DiffDataCenterCount+1 > len(dcCounts) {
return false
}
if rp.DiffRackCount+1 > len(rackCounts) {
return false
}
if rp.SameRackCount > 0 {
for _, rackCount := range rackCounts {
if rackCount >= rp.SameRackCount+1 {
return true
}
}
return false
}
return true
}
// PickOneReplicaToCopyFrom returns the most recently modified replica, the
// best source for adding a new replica.
func PickOneReplicaToCopyFrom(replicas []Replica) int {
mostRecent := -1
for i, r := range replicas {
if mostRecent < 0 || r.ModifiedAtSecond > replicas[mostRecent].ModifiedAtSecond {
mostRecent = i
}
}
return mostRecent
}
// IsMisplaced reports whether any replica sits at a location inconsistent
// with the replication policy given the other replicas' locations.
func IsMisplaced(replicas []Replica, rp *super_block.ReplicaPlacement) bool {
for i := range replicas {
others := otherThan(replicas, i)
locs := make([]Location, len(others))
for j, r := range others {
locs[j] = r.Location
}
if !SatisfyReplicaPlacement(rp, locs, replicas[i].Location) {
return true
}
}
return false
}
// PickSmallestReplica returns the smallest replica (ties broken by oldest
// then lowest compact revision).
func PickSmallestReplica(replicas []Replica) int {
smallest := -1
for i := range replicas {
if smallest < 0 || lessReplica(replicas[i], replicas[smallest]) {
smallest = i
}
}
return smallest
}
// PickOneReplicaToDelete selects the replica to trim when over-replicated.
// It only ever removes the smallest of multiple healthy writable replicas: a
// ReadOnly/integrity-flagged replica is never chosen for deletion, and the
// trim is refused (returns -1) when removing a writable replica would leave
// only ReadOnly survivors. Among the writable replicas it prefers one whose
// removal still satisfies placement, so the trim does not strip the only
// replica in a required failure domain; it falls back to the smallest
// writable if none keeps placement (a later misplaced cycle then
// re-balances).
func PickOneReplicaToDelete(replicas []Replica, rp *super_block.ReplicaPlacement) int {
var writable []int
for i, r := range replicas {
if !r.ReadOnly {
writable = append(writable, i)
}
}
if len(writable) < 2 {
return -1
}
var placementSafe []int
for _, i := range writable {
if !IsMisplaced(otherThan(replicas, i), rp) {
placementSafe = append(placementSafe, i)
}
}
if len(placementSafe) > 0 {
return pickSmallestAmong(replicas, placementSafe)
}
return pickSmallestAmong(replicas, writable)
}
// PickOneMisplacedVolume selects the replica to delete and recreate at a
// correct placement. This is relocation, not over-replication: unlike
// PickOneReplicaToDelete it must still act on read-only replicas (e.g. a full
// but misplaced volume), so it does not use the writable-survivor guard. It
// prefers a replica whose removal leaves the others well placed, and falls
// back to the smallest replica.
func PickOneMisplacedVolume(replicas []Replica, rp *super_block.ReplicaPlacement) int {
var deletionCandidates []int
for i := range replicas {
if !IsMisplaced(otherThan(replicas, i), rp) {
deletionCandidates = append(deletionCandidates, i)
}
}
if smallest := pickSmallestAmong(replicas, deletionCandidates); smallest >= 0 {
return smallest
}
return PickSmallestReplica(replicas)
}
func pickSmallestAmong(replicas []Replica, candidates []int) int {
smallest := -1
for _, i := range candidates {
if smallest < 0 || lessReplica(replicas[i], replicas[smallest]) {
smallest = i
}
}
return smallest
}
func lessReplica(a, b Replica) bool {
if a.Size != b.Size {
return a.Size < b.Size
}
if a.ModifiedAtSecond != b.ModifiedAtSecond {
return a.ModifiedAtSecond < b.ModifiedAtSecond
}
return a.CompactRevision < b.CompactRevision
}
func otherThan(replicas []Replica, index int) (others []Replica) {
for i := range replicas {
if index != i {
others = append(others, replicas[i])
}
}
return
}

View File

@@ -0,0 +1,65 @@
package topology
import (
"encoding/json"
"fmt"
hashicorpRaft "github.com/hashicorp/raft"
"github.com/seaweedfs/raft"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
type MaxVolumeIdCommand struct {
MaxVolumeId needle.VolumeId `json:"maxVolumeId"`
TopologyId string `json:"topologyId"`
}
func NewMaxVolumeIdCommand(value needle.VolumeId, topologyId string) *MaxVolumeIdCommand {
return &MaxVolumeIdCommand{
MaxVolumeId: value,
TopologyId: topologyId,
}
}
func (c *MaxVolumeIdCommand) CommandName() string {
return "MaxVolumeId"
}
// deprecatedCommandApply represents the old interface to apply a command to the server.
func (c *MaxVolumeIdCommand) Apply(server raft.Server) (interface{}, error) {
topo := server.Context().(*Topology)
before := topo.GetMaxVolumeId()
topo.UpAdjustMaxVolumeId(c.MaxVolumeId)
if c.TopologyId != "" {
prevTopologyId := topo.GetTopologyId()
topo.SetTopologyId(c.TopologyId)
// Log when TopologyId is set for the first time, with different messages for leader and follower.
if prevTopologyId == "" {
if server.State() == raft.Leader {
glog.V(0).Infof("TopologyId generated and applied on leader: %s", c.TopologyId)
} else {
glog.V(0).Infof("TopologyId applied on follower: %s", c.TopologyId)
}
}
}
glog.V(1).Infoln("max volume id", before, "==>", topo.GetMaxVolumeId())
return nil, nil
}
func (s *MaxVolumeIdCommand) Persist(sink hashicorpRaft.SnapshotSink) error {
b, err := json.Marshal(s)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
_, err = sink.Write(b)
if err != nil {
sink.Cancel()
return fmt.Errorf("sink.Write(): %w", err)
}
return sink.Close()
}
func (s *MaxVolumeIdCommand) Release() {
}

107
weed/topology/collection.go Normal file
View File

@@ -0,0 +1,107 @@
package topology
import (
"fmt"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/util"
)
type Collection struct {
Name string
volumeSizeLimit uint64
replicationAsMin bool
storageType2VolumeLayout *util.ConcurrentReadMap
}
func NewCollection(name string, volumeSizeLimit uint64, replicationAsMin bool) *Collection {
c := &Collection{
Name: name,
volumeSizeLimit: volumeSizeLimit,
replicationAsMin: replicationAsMin,
}
c.storageType2VolumeLayout = util.NewConcurrentReadMap()
return c
}
func (c *Collection) String() string {
return fmt.Sprintf("Name:%s, volumeSizeLimit:%d, storageType2VolumeLayout:%v", c.Name, c.volumeSizeLimit, c.storageType2VolumeLayout)
}
func (c *Collection) GetOrCreateVolumeLayout(rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType) *VolumeLayout {
keyString := rp.String()
if ttl != nil {
keyString += ttl.String()
}
if diskType != types.HardDriveType {
keyString += string(diskType)
}
vl := c.storageType2VolumeLayout.Get(keyString, func() interface{} {
return NewVolumeLayout(rp, ttl, diskType, c.volumeSizeLimit, c.replicationAsMin)
})
return vl.(*VolumeLayout)
}
func (c *Collection) GetVolumeLayout(rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType) (*VolumeLayout, bool) {
keyString := rp.String()
if ttl != nil {
keyString += ttl.String()
}
if diskType != types.HardDriveType {
keyString += string(diskType)
}
vl, ok := c.storageType2VolumeLayout.Find(keyString)
if !ok {
return nil, false
}
return vl.(*VolumeLayout), true
}
func (c *Collection) GetAllVolumeLayouts() []*VolumeLayout {
var vls []*VolumeLayout
for _, vl := range c.storageType2VolumeLayout.Items() {
if vl != nil {
vls = append(vls, vl.(*VolumeLayout))
}
}
return vls
}
func (c *Collection) DeleteVolumeLayout(rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType) {
keyString := rp.String()
if ttl != nil {
keyString += ttl.String()
}
if diskType != types.HardDriveType {
keyString += string(diskType)
}
// Unpublish first so a racing registration re-resolves into a fresh layout.
if vl, found := c.storageType2VolumeLayout.Delete(keyString); found {
vl.(*VolumeLayout).releaseLookupOwnership()
}
}
func (c *Collection) Lookup(vid needle.VolumeId) []*DataNode {
for _, vl := range c.storageType2VolumeLayout.Items() {
if vl != nil {
if list := vl.(*VolumeLayout).Lookup(vid); list != nil {
return list
}
}
}
return nil
}
func (c *Collection) ListVolumeServers() (nodes []*DataNode) {
for _, vl := range c.storageType2VolumeLayout.Items() {
if vl != nil {
if list := vl.(*VolumeLayout).ListVolumeServers(); list != nil {
nodes = append(nodes, list...)
}
}
}
return
}

View File

@@ -0,0 +1,72 @@
package topology
import (
"sync"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
func TestGetVolumeLayoutOfAbsentKey(t *testing.T) {
rp, _ := super_block.NewReplicaPlacementFromString("000")
c := NewCollection("c", 32*1024, false)
vl, found := c.GetVolumeLayout(rp, needle.EMPTY_TTL, types.HardDriveType)
if found || vl != nil {
t.Fatalf("absent layout: got (%v, %v), want (nil, false)", vl, found)
}
}
// Volume servers dropping the last replica of several volumes that share one
// layout all see the layout go empty and all delete it. The losers used to
// crash the master on a nil type assertion.
func TestConcurrentLastReplicaRemoval(t *testing.T) {
const nodeCount = 8
ttl, _ := needle.ReadTTL("5m")
for round := 0; round < 500; round++ {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
// A second layout keeps the collection alive, so every remover reaches
// the layout deletion instead of stopping at a vanished collection.
keeper := rack.GetOrCreateDataNode("127.0.0.1", 9000, 0, "", "", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{{
Id: 999, Collection: "c", Version: uint32(needle.GetCurrentVersion()), Ttl: ttl.ToUint32(),
}}, keeper)
var wg sync.WaitGroup
start := make(chan struct{})
for i := 0; i < nodeCount; i++ {
dn := rack.GetOrCreateDataNode("127.0.0.1", 8080+i, 0, "", "", map[string]uint32{"": 100})
m := &master_pb.VolumeInformationMessage{
Id: uint32(i + 1), Collection: "c", Version: uint32(needle.GetCurrentVersion()),
}
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{m}, dn)
vi, err := storage.NewVolumeInfo(m)
if err != nil {
t.Fatalf("NewVolumeInfo: %v", err)
}
wg.Add(1)
go func() {
defer wg.Done()
<-start
topo.UnRegisterVolumeLayout(vi, dn)
}()
}
close(start)
wg.Wait()
c, found := topo.FindCollection("c")
if !found {
t.Fatalf("round %d: collection dropped while a layout still had volumes", round)
}
if layouts := c.GetAllVolumeLayouts(); len(layouts) != 1 {
t.Fatalf("round %d: got %d layouts, want only the one still holding a volume", round, len(layouts))
}
}
}

View File

@@ -0,0 +1,148 @@
package topology
import (
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
// CollectionStatistics is what a collection holds, summarised so that callers
// tracking usage do not have to be sent every volume in the cluster to add it
// up themselves.
type CollectionStatistics struct {
Collection string
FileCount uint64
DeleteCount uint64
DeletedByteCount uint64
// Size counts one copy of the data: a single replica of a regular volume,
// the data shards of an ec volume.
Size uint64
// PhysicalSize counts what is on disk: every replica, and parity shards.
PhysicalSize uint64
VolumeCount uint64
}
type ecStatsKey struct {
collection string
volumeId needle.VolumeId
}
// ecFileCounts holds the per-volume counts that can only be resolved once every
// shard holder has been seen.
type ecFileCounts struct {
collection string
fileCount uint64
deleteCount uint64
}
// CollectionStatistics summarises every collection in one pass over the
// topology, allocating per collection rather than per volume.
func (t *Topology) CollectionStatistics() []*CollectionStatistics {
byCollection := make(map[string]*CollectionStatistics)
statsFor := func(collection string) *CollectionStatistics {
stats, found := byCollection[collection]
if !found {
stats = &CollectionStatistics{Collection: collection}
byCollection[collection] = stats
}
return stats
}
// Regular volumes are counted once each for logical totals and once per
// replica for physical, which the lookup index gives without a set of seen
// ids: it is already keyed by volume.
for _, c := range t.collectionMap.Items() {
collection := c.(*Collection)
for _, vl := range collection.GetAllVolumeLayouts() {
vl.accessLock.RLock()
for vid, locations := range vl.vid2location {
stats := statsFor(collection.Name)
// Replicas of one volume can disagree while a write is landing
// or a heartbeat is late. Count the one holding the most live
// data, so the answer does not depend on which replica is
// looked at first and usage is never reported lower than some
// replica already holds. Quotas are enforced on size less
// deletions, so that is what has to be the largest -- a replica
// with the biggest raw size can be the one that has deleted the
// most, and picking it would leave an over-quota bucket
// writable.
var largest storage.VolumeInfo
var largestLive uint64
found := false
for _, dn := range locations.list {
v, err := dn.GetVolumesById(vid)
if err != nil {
continue
}
stats.PhysicalSize += v.Size
live := v.Size
if v.DeletedByteCount < live {
live -= v.DeletedByteCount
} else {
live = 0
}
if !found || live > largestLive {
largest, largestLive, found = v, live, true
}
}
if !found {
continue
}
stats.Size += largest.Size
stats.FileCount += uint64(largest.FileCount)
stats.DeleteCount += uint64(largest.DeleteCount)
// Never more deletions than the volume holds. Live usage is
// read as the collection's size less its deletions, so a volume
// reporting more deleted bytes than it has would cancel live
// bytes belonging to other volumes and report the bucket
// smaller than it is.
stats.DeletedByteCount += largest.Size - largestLive
stats.VolumeCount++
}
vl.accessLock.RUnlock()
}
}
// Ec shards are node-local rather than replicated, so their sizes sum
// across holders. The file and delete counts describe the volume rather
// than the shard, so they resolve once every holder has been seen.
perEcVolume := make(map[ecStatsKey]*ecFileCounts)
for _, dcNode := range t.Children() {
for _, rackNode := range dcNode.(*DataCenter).Children() {
for _, dnNode := range rackNode.(*Rack).Children() {
for _, ecInfo := range dnNode.(*DataNode).GetEcShards() {
message := ecInfo.ToVolumeEcShardInformationMessage()
stats := statsFor(ecInfo.Collection)
stats.PhysicalSize += uint64(erasure_coding.EcShardsTotalSize(message))
stats.Size += uint64(erasure_coding.EcShardsDataSize(message, 0))
key := ecStatsKey{collection: ecInfo.Collection, volumeId: ecInfo.VolumeId}
counts, found := perEcVolume[key]
if !found {
counts = &ecFileCounts{collection: ecInfo.Collection}
perEcVolume[key] = counts
stats.VolumeCount++
}
if message.FileCount > counts.fileCount {
counts.fileCount = message.FileCount
}
counts.deleteCount += message.DeleteCount
}
}
}
}
for _, counts := range perEcVolume {
stats := byCollection[counts.collection]
if stats == nil {
continue
}
stats.FileCount += counts.fileCount
stats.DeleteCount += counts.deleteCount
}
ret := make([]*CollectionStatistics, 0, len(byCollection))
for _, stats := range byCollection {
ret = append(ret, stats)
}
return ret
}

View File

@@ -0,0 +1,309 @@
package topology
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
)
// referenceCollectionStatistics is what callers computed for themselves from a
// full topology listing, kept here so the summary can be held to producing the
// same numbers. Quota enforcement reads these.
func referenceCollectionStatistics(t *master_pb.TopologyInfo) map[string]*CollectionStatistics {
type volumeKey struct {
collection string
volumeId uint32
}
out := map[string]*CollectionStatistics{}
seen := map[volumeKey]bool{}
ecCounts := map[volumeKey]*ecFileCounts{}
statsFor := func(c string) *CollectionStatistics {
s, ok := out[c]
if !ok {
s = &CollectionStatistics{Collection: c}
out[c] = s
}
return s
}
for _, dc := range t.DataCenterInfos {
for _, r := range dc.RackInfos {
for _, dn := range r.DataNodeInfos {
for _, diskInfo := range dn.DiskInfos {
for _, vi := range diskInfo.VolumeInfos {
s := statsFor(vi.Collection)
s.PhysicalSize += vi.Size
key := volumeKey{vi.Collection, vi.Id}
if seen[key] {
continue
}
seen[key] = true
s.Size += vi.Size
s.FileCount += vi.FileCount
s.DeleteCount += vi.DeleteCount
s.DeletedByteCount += vi.DeletedByteCount
s.VolumeCount++
}
for _, esi := range diskInfo.EcShardInfos {
s := statsFor(esi.Collection)
s.PhysicalSize += uint64(erasure_coding.EcShardsTotalSize(esi))
s.Size += uint64(erasure_coding.EcShardsDataSize(esi, 0))
key := volumeKey{esi.Collection, esi.Id}
agg, ok := ecCounts[key]
if !ok {
agg = &ecFileCounts{collection: esi.Collection}
ecCounts[key] = agg
s.VolumeCount++
}
if esi.FileCount > agg.fileCount {
agg.fileCount = esi.FileCount
}
agg.deleteCount += esi.DeleteCount
}
}
}
}
}
for _, agg := range ecCounts {
if s := out[agg.collection]; s != nil {
s.FileCount += agg.fileCount
s.DeleteCount += agg.deleteCount
}
}
return out
}
func statsTopology(t *testing.T) *Topology {
t.Helper()
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
nodes := make([]*DataNode, 3)
for i := range nodes {
nodes[i] = rack.GetOrCreateDataNode(
"10.0.0."+string(rune('1'+i)), 8080, 18080, "", "", map[string]uint32{"": 1000, "ssd": 1000})
}
// Two collections, replicated volumes, one volume on a second disk type.
// Replicas agree here; disagreement is covered on its own, where the
// listing this is compared against is itself order dependent.
report := func(dn *DataNode, msgs ...*master_pb.VolumeInformationMessage) {
topo.SyncDataNodeRegistration(msgs, dn)
}
vol := func(id uint32, collection string, size uint64, diskType string) *master_pb.VolumeInformationMessage {
return &master_pb.VolumeInformationMessage{
Id: id, Collection: collection, Size: size, FileCount: uint64(id) * 10,
DeleteCount: uint64(id), DeletedByteCount: uint64(id) * 100,
Version: 3, ReplicaPlacement: 1, DiskType: diskType,
}
}
report(nodes[0], vol(1, "bucket-a", 1000, ""), vol(2, "bucket-a", 2000, ""), vol(5, "bucket-b", 700, "ssd"))
report(nodes[1], vol(1, "bucket-a", 1000, ""), vol(3, "bucket-b", 3000, ""))
report(nodes[2], vol(2, "bucket-a", 2000, ""), vol(3, "bucket-b", 3000, ""))
// Ec shards for a third collection, spread over two nodes, with the file
// count reported differently by each holder.
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 9, Collection: "bucket-c", EcIndexBits: 0x1f, ShardSizes: []int64{10, 20, 30, 40, 50}, FileCount: 40, DeleteCount: 2},
}, nodes[0])
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 9, Collection: "bucket-c", EcIndexBits: 0x3e0, ShardSizes: []int64{60, 70, 80, 90, 100}, FileCount: 44, DeleteCount: 3},
}, nodes[1])
return topo
}
// The summary replaces callers adding up a full topology listing, so it has to
// produce what that produced: these numbers enforce bucket quotas.
func TestCollectionStatisticsMatchesAFullListing(t *testing.T) {
topo := statsTopology(t)
want := referenceCollectionStatistics(topo.ToTopologyInfo(VolumeFilter{}))
got := map[string]*CollectionStatistics{}
for _, s := range topo.CollectionStatistics() {
got[s.Collection] = s
}
if len(got) != len(want) {
t.Fatalf("summarised %d collections, the listing had %d: %v vs %v", len(got), len(want), got, want)
}
for name, expected := range want {
actual, found := got[name]
if !found {
t.Errorf("collection %s is missing from the summary", name)
continue
}
if *actual != *expected {
t.Errorf("collection %s:\n summary %+v\n listing %+v", name, *actual, *expected)
}
}
}
// Replicas disagree while a write is landing or a heartbeat is late. The full
// listing walked the topology in map order and took whichever replica it
// reached first, so its answer was not stable; the summary takes the largest.
func TestCollectionStatisticsPicksTheLargestReplica(t *testing.T) {
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
small := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100})
large := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "b", map[string]uint32{"": 100})
behind := &master_pb.VolumeInformationMessage{
Id: 1, Collection: "bucket-a", Size: 1000, FileCount: 10, Version: 3, ReplicaPlacement: 1,
}
ahead := &master_pb.VolumeInformationMessage{
Id: 1, Collection: "bucket-a", Size: 4000, FileCount: 40, Version: 3, ReplicaPlacement: 1,
}
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{behind}, small)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{ahead}, large)
for i := 0; i < 8; i++ {
stats := topo.CollectionStatistics()
if len(stats) != 1 {
t.Fatalf("expected one collection, got %d", len(stats))
}
if stats[0].Size != 4000 || stats[0].FileCount != 40 {
t.Fatalf("run %d reported size %d files %d, want the larger replica's 4000 and 40",
i, stats[0].Size, stats[0].FileCount)
}
if stats[0].PhysicalSize != 5000 {
t.Fatalf("run %d reported physical size %d, want both replicas summed", i, stats[0].PhysicalSize)
}
}
}
func statsByCollection(topo *Topology) map[string]*CollectionStatistics {
out := map[string]*CollectionStatistics{}
for _, s := range topo.CollectionStatistics() {
out[s.Collection] = s
}
return out
}
// The .ecx and .ecj travel with the shards, so a holder still loading them
// reports zero and must not pin the volume's count down.
func TestCollectionStatisticsTakesTheLargestEcFileCount(t *testing.T) {
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
loading := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100})
loaded := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "b", map[string]uint32{"": 100})
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 11, Collection: "bucket-b", EcIndexBits: 0x7f, ShardSizes: []int64{1, 1, 1, 1, 1, 1, 1}, FileCount: 0},
}, loading)
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 11, Collection: "bucket-b", EcIndexBits: 0x3f80, ShardSizes: []int64{1, 1, 1, 1, 1, 1, 1}, FileCount: 6},
}, loaded)
stats := statsByCollection(topo)["bucket-b"]
if stats == nil {
t.Fatal("expected bucket-b to be reported")
}
if stats.FileCount != 6 {
t.Errorf("file count %d, want the largest any holder reported (6)", stats.FileCount)
}
if stats.VolumeCount != 1 {
t.Errorf("volume count %d, want one volume however many holders report shards", stats.VolumeCount)
}
// 14 shards of 1 byte each; only the 10 data shards count as logical.
if stats.PhysicalSize != 14 {
t.Errorf("physical size %d, want every shard counted (14)", stats.PhysicalSize)
}
if stats.Size != 10 {
t.Errorf("size %d, want the data shards only (10)", stats.Size)
}
}
// A collection with both kinds has to have them added together.
func TestCollectionStatisticsCountsRegularAndEcTogether(t *testing.T) {
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
{Id: 1, Collection: "mixed", Size: 500, FileCount: 5, DeleteCount: 1, DeletedByteCount: 50, Version: 3},
}, dn)
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 2, Collection: "mixed", EcIndexBits: 0x3fff,
ShardSizes: []int64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, FileCount: 7, DeleteCount: 2},
}, dn)
stats := statsByCollection(topo)["mixed"]
if stats == nil {
t.Fatal("expected the mixed collection to be reported")
}
if stats.VolumeCount != 2 {
t.Errorf("volume count %d, want the regular and the ec volume (2)", stats.VolumeCount)
}
if stats.FileCount != 12 {
t.Errorf("file count %d, want 5 regular plus 7 ec", stats.FileCount)
}
if stats.DeleteCount != 3 {
t.Errorf("delete count %d, want 1 regular plus 2 ec", stats.DeleteCount)
}
if stats.Size != 510 {
t.Errorf("size %d, want 500 regular plus 10 data shards", stats.Size)
}
if stats.PhysicalSize != 514 {
t.Errorf("physical size %d, want 500 regular plus all 14 shards", stats.PhysicalSize)
}
}
// Quotas are enforced on size less deletions, so the replica holding the most
// live data is the one to count. The replica with the biggest raw size can be
// the one that has deleted the most, and counting that one would report a
// bucket smaller than it is and leave it writable over its quota.
func TestCollectionStatisticsPicksTheReplicaHoldingTheMostLiveData(t *testing.T) {
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
bigMostlyDeleted := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100})
smallerButLive := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "b", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{{
Id: 1, Collection: "bucket-a", Size: 1000, DeletedByteCount: 900,
Version: 3, ReplicaPlacement: 1,
}}, bigMostlyDeleted)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{{
Id: 1, Collection: "bucket-a", Size: 900, DeletedByteCount: 0,
Version: 3, ReplicaPlacement: 1,
}}, smallerButLive)
stats := statsByCollection(topo)["bucket-a"]
if stats == nil {
t.Fatal("expected bucket-a to be reported")
}
live := stats.Size - stats.DeletedByteCount
if live != 900 {
t.Errorf("reported %d bytes live (size %d less %d deleted), want the 900 one replica holds",
live, stats.Size, stats.DeletedByteCount)
}
if stats.PhysicalSize != 1900 {
t.Errorf("physical size %d, want both replicas summed", stats.PhysicalSize)
}
}
// Live usage is read as the collection's size less its deletions, so a volume
// reporting more deleted bytes than it holds must not cancel live bytes
// belonging to other volumes in the same bucket.
func TestCollectionStatisticsDeletionsNeverExceedTheVolume(t *testing.T) {
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
// More deleted bytes than the volume holds, which a compaction can
// leave behind, and a healthy volume beside it.
{Id: 1, Collection: "bucket-a", Size: 100, DeletedByteCount: 500, Version: 3},
{Id: 2, Collection: "bucket-a", Size: 1000, DeletedByteCount: 0, Version: 3},
}, dn)
stats := statsByCollection(topo)["bucket-a"]
if stats == nil {
t.Fatal("expected bucket-a to be reported")
}
if stats.DeletedByteCount > stats.Size {
t.Errorf("deletions %d exceed size %d, so live usage reads as zero", stats.DeletedByteCount, stats.Size)
}
if live := stats.Size - stats.DeletedByteCount; live != 1000 {
t.Errorf("reported %d bytes live, want the 1000 the second volume holds", live)
}
}

View File

@@ -0,0 +1,40 @@
package topology
import (
"encoding/xml"
)
type rack struct {
Name string `xml:"name,attr"`
Ips []string `xml:"Ip"`
}
type dataCenter struct {
Name string `xml:"name,attr"`
Racks []rack `xml:"Rack"`
}
type topology struct {
DataCenters []dataCenter `xml:"DataCenter"`
}
type Configuration struct {
XMLName xml.Name `xml:"Configuration"`
Topo topology `xml:"Topology"`
}
func (c *Configuration) String() string {
if b, e := xml.MarshalIndent(c, " ", " "); e == nil {
return string(b)
}
return ""
}
func (c *Configuration) Locate(ip string, dcName string, rackName string) (dc string, rack string) {
if dcName == "" {
dcName = "DefaultDataCenter"
}
if rackName == "" {
rackName = "DefaultRack"
}
return dcName, rackName
}

View File

@@ -0,0 +1,125 @@
package topology
import (
"sync"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
// Crowding asks for more room to write into, so a read-only volume has nothing
// to say about it. Growth already discounts them by intersecting with the
// writable list, so an entry for one is weight and nothing else.
func TestCrowdedVolumesAreOnesThatCanTakeWrites(t *testing.T) {
topo := NewTopology("crowd", nil, 10000, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("127.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 100})
// Both are past the growth threshold; only one can be written to.
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
{Id: 1, Size: 9500, Collection: "c", Version: 3},
{Id: 2, Size: 9500, Collection: "c", Version: 3, ReadOnly: true},
}, dn)
dn.LastSeen = time.Now().Unix()
// The channels are unbuffered and normally drained by the writable-refresh
// loop, which is not running here.
crowded := map[needle.VolumeId]bool{}
stop, done := make(chan struct{}), make(chan struct{})
go func() {
defer close(done)
for {
select {
case v := <-topo.chanCrowdedVolumes:
crowded[v.Id] = true
case <-topo.chanFullVolumes:
case <-stop:
return
}
}
}()
// The channels are unbuffered, so every send has been received by the time
// this returns; waiting for the collector to finish covers the recording.
topo.CollectDeadNodeAndFullVolumes(time.Now().Unix()-1, topo.volumeSizeLimit, 0.9)
close(stop)
<-done // the collector owns the map until it returns
if !crowded[needle.VolumeId(1)] {
t.Error("a writable volume past the threshold was not reported crowded")
}
if crowded[needle.VolumeId(2)] {
t.Error("a read-only volume was reported crowded, which only adds an entry growth then discounts")
}
}
// The count growth decides on has to be unchanged by this.
func TestCrowdedCountStillMatchesWritables(t *testing.T) {
topo := NewTopology("crowd", nil, 10000, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("127.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
{Id: 1, Size: 9500, Collection: "c", Version: 3},
}, dn)
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)
vl.SetVolumeCrowded(needle.VolumeId(1))
writable, crowded := vl.GetWritableVolumeCount()
if writable != 1 || crowded != 1 {
t.Errorf("writable=%d crowded=%d, want 1 and 1", writable, crowded)
}
}
// SetVolumeCrowded mutates the crowded map while GetWritableVolumeCount reads
// it on the Assign hot path. Under RLock both could run at once and the Go
// runtime aborts with "concurrent map read and map write". Run with -race to
// reproduce the original bug; the write lock makes this pass.
func TestSetVolumeCrowdedNoRaceWithGetWritableVolumeCount(t *testing.T) {
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := NewVolumeLayout(rp, needle.EMPTY_TTL, types.HardDriveType, 10000, false)
// GetWritableVolumeCount iterates vl.writables, so give it something to
// read while the crowded map is being mutated.
vl.writables = []needle.VolumeId{1, 2, 3}
var wg sync.WaitGroup
stop := make(chan struct{})
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
vl.SetVolumeCrowded(needle.VolumeId(2))
}
}
}()
}
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
vl.GetWritableVolumeCount()
}
}
}()
}
// The race detector needs only a brief window to flag a concurrent
// map read/write; let the goroutines hammer the lock briefly.
time.Sleep(50 * time.Millisecond)
close(stop)
wg.Wait()
}

View File

@@ -0,0 +1,69 @@
package topology
import (
"slices"
"strings"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
)
type DataCenter struct {
NodeImpl
}
func NewDataCenter(id string) *DataCenter {
dc := &DataCenter{}
dc.id = NodeId(id)
dc.nodeType = "DataCenter"
dc.diskUsages = newDiskUsages()
dc.children = make(map[NodeId]Node)
dc.capacityReservations = newCapacityReservations()
dc.NodeImpl.value = dc
return dc
}
func (dc *DataCenter) GetOrCreateRack(rackName string) *Rack {
dc.Lock()
defer dc.Unlock()
for _, c := range dc.children {
rack := c.(*Rack)
if string(rack.Id()) == rackName {
return rack
}
}
rack := NewRack(rackName)
dc.doLinkChildNode(rack)
return rack
}
type DataCenterInfo struct {
Id NodeId `json:"Id"`
Racks []RackInfo `json:"Racks"`
}
func (dc *DataCenter) ToInfo() (info DataCenterInfo) {
info.Id = dc.Id()
var racks []RackInfo
for _, c := range dc.Children() {
rack := c.(*Rack)
racks = append(racks, rack.ToInfo())
}
slices.SortFunc(racks, func(a, b RackInfo) int {
return strings.Compare(string(a.Id), string(b.Id))
})
info.Racks = racks
return
}
func (dc *DataCenter) ToDataCenterInfo(filter VolumeFilter) *master_pb.DataCenterInfo {
m := &master_pb.DataCenterInfo{
Id: string(dc.Id()),
DiskInfos: dc.diskUsages.ToDiskInfo(),
}
for _, c := range dc.Children() {
rack := c.(*Rack)
m.RackInfos = append(m.RackInfos, rack.ToRackInfo(filter))
}
return m
}

482
weed/topology/data_node.go Normal file
View File

@@ -0,0 +1,482 @@
package topology
import (
"fmt"
"sync/atomic"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/util"
)
type DataNode struct {
NodeImpl
Ip string
Port int
GrpcPort int
PublicUrl string
LastSeen int64 // unix time in seconds
Counter int // in race condition, the previous dataNode was not dead
IsTerminating bool
// maintenanceMode mirrors the volume server's own flag, reported over the
// heartbeat. A server in maintenance is being drained: the master places
// no new volumes on it and assigns no writes to the volumes it holds. Read
// on the assign and volume-growth paths without the node lock.
maintenanceMode atomic.Bool
// lookupDigest covers the volumes reachable through this node in the volume
// layouts, for comparison against what its disks actually hold.
lookupDigest atomic.Uint64
// duplicateVolumeIds records that the node last reported one volume id more
// than once, which the master cannot represent.
duplicateVolumeIds atomic.Bool
// diskMetas holds each physical disk's tags, type, and capacity from the
// heartbeat DiskTags, including disks with no volumes or EC shards.
diskMetas map[uint32]diskMeta
}
type diskMeta struct {
tags []string
diskType types.DiskType
maxVolumeCount int64
}
func NewDataNode(id string) *DataNode {
dn := &DataNode{}
dn.id = NodeId(id)
dn.nodeType = "DataNode"
dn.diskUsages = newDiskUsages()
dn.children = make(map[NodeId]Node)
dn.capacityReservations = newCapacityReservations()
dn.NodeImpl.value = dn
return dn
}
func (dn *DataNode) String() string {
dn.RLock()
defer dn.RUnlock()
return fmt.Sprintf("Node:%s, Ip:%s, Port:%d, PublicUrl:%s", dn.NodeImpl.String(), dn.Ip, dn.Port, dn.PublicUrl)
}
// InMaintenanceMode reports whether the volume server asked to be left alone
// for writes; see Topology.SetDataNodeMaintenanceMode for what that changes.
func (dn *DataNode) InMaintenanceMode() bool {
return dn.maintenanceMode.Load()
}
// SetMaintenanceMode records the flag and reports whether it changed. Prefer
// Topology.SetDataNodeMaintenanceMode, which also updates the writable lists.
func (dn *DataNode) SetMaintenanceMode(on bool) (changed bool) {
return dn.maintenanceMode.Swap(on) != on
}
func (dn *DataNode) AddOrUpdateVolume(v storage.VolumeInfo) (isNew, isChangedRO, tierTransition bool) {
dn.Lock()
defer dn.Unlock()
return dn.doAddOrUpdateVolume(v)
}
// SetVolumeReadOnly records the read-only flag a volume server reported for
// one of its volumes, ahead of the heartbeat that will repeat it.
func (dn *DataNode) SetVolumeReadOnly(vid needle.VolumeId, readOnly bool) {
dn.Lock()
defer dn.Unlock()
for _, c := range dn.children {
if c.(*Disk).SetVolumeReadOnly(vid, readOnly) {
return
}
}
}
func (dn *DataNode) getOrCreateDisk(diskType string) *Disk {
c, found := dn.children[NodeId(diskType)]
if !found {
c = NewDisk(diskType)
dn.doLinkChildNode(c)
}
disk := c.(*Disk)
return disk
}
func (dn *DataNode) doAddOrUpdateVolume(v storage.VolumeInfo) (isNew, isChanged, tierTransition bool) {
disk := dn.getOrCreateDisk(v.DiskType)
return disk.AddOrUpdateVolume(v)
}
// AddProvisionalVolume records a volume the master registered on its own,
// ahead of any server report naming it. See Disk.AddProvisionalVolume.
func (dn *DataNode) AddProvisionalVolume(v storage.VolumeInfo) (isNew, isChanged, tierTransition bool) {
dn.Lock()
defer dn.Unlock()
disk := dn.getOrCreateDisk(v.DiskType)
return disk.AddProvisionalVolume(v)
}
// UpdateVolumes detects new/deleted/changed volumes on a volume server
// used in master to notify master clients of these changes.
//
// changedVolumes covers every replica the disk already held whose
// classification the new report altered in a way clients must learn about:
// the ReadOnly flag flipped, or IsRemote() flipped on tier transition. The
// latter is what lets the wdclient refresh DataInRemote after the digest
// mismatch recovery path resends a full Volumes list -- that path is the
// only way a re-tiered replica reaches the master without a separate
// ChangedVolumes heartbeat.
func (dn *DataNode) UpdateVolumes(actualVolumes []storage.VolumeInfo) (newVolumes, deletedVolumes, changedVolumes []storage.VolumeInfo) {
reported := newReportedVolumes(len(actualVolumes))
for _, v := range actualVolumes {
reported.add(v.Id, v.DiskType)
}
// A volume id mounted on two disks of one server -- a stale twin re-attached
// after a disk repair -- is reported twice, but the master keys volumes by
// id alone and keeps only the last copy. Its digest can then never equal the
// server's however often the list is resent, so record it and let the
// heartbeat fall back to the full list for this node.
dn.duplicateVolumeIds.Store(reported.duplicated)
dn.Lock()
defer dn.Unlock()
keptCount := 0
for _, c := range dn.children {
disk := c.(*Disk)
for _, v := range disk.RemoveVolumesNotIn(reported) {
glog.V(0).Infoln("Deleting volume id:", v.Id)
deletedVolumes = append(deletedVolumes, v)
deltaDiskUsage := &DiskUsageCounts{}
deltaDiskUsage.volumeCount = -1
if v.IsRemote() {
deltaDiskUsage.remoteVolumeCount = -1
}
if !v.ReadOnly {
deltaDiskUsage.activeVolumeCount = -1
}
disk.UpAdjustDiskUsageDelta(types.ToDiskType(v.DiskType), deltaDiskUsage)
}
keptCount += disk.VolumeCount()
}
// Everything still on the node is also in this heartbeat, so the remainder
// is what the node is about to gain. A steady-state heartbeat gains nothing
// and must not allocate here; a reconnecting server gains all of them.
if addedCount := reported.count() - keptCount; addedCount > 0 {
newVolumes = make([]storage.VolumeInfo, 0, addedCount)
}
for _, v := range actualVolumes {
isNew, isChanged, tierTransition := dn.doAddOrUpdateVolume(v)
if isNew {
newVolumes = append(newVolumes, v)
}
if isChanged || tierTransition {
changedVolumes = append(changedVolumes, v)
}
}
return
}
func (dn *DataNode) DeltaUpdateVolumes(newVolumes, deletedVolumes []storage.VolumeInfo) {
dn.Lock()
defer dn.Unlock()
for _, v := range deletedVolumes {
disk := dn.getOrCreateDisk(v.DiskType)
_, err := disk.GetVolumesById(v.Id)
if err != nil {
continue
}
disk.DeleteVolumeById(v.Id)
deltaDiskUsage := &DiskUsageCounts{}
deltaDiskUsage.volumeCount = -1
if v.IsRemote() {
deltaDiskUsage.remoteVolumeCount = -1
}
if !v.ReadOnly {
deltaDiskUsage.activeVolumeCount = -1
}
disk.UpAdjustDiskUsageDelta(types.ToDiskType(v.DiskType), deltaDiskUsage)
}
for _, v := range newVolumes {
dn.doAddOrUpdateVolume(v)
}
return
}
func (dn *DataNode) AdjustMaxVolumeCounts(maxVolumeCounts map[string]uint32) {
for diskType, maxVolumeCount := range maxVolumeCounts {
if maxVolumeCount == 0 {
// the volume server may have set the max to zero
continue
}
dt := types.ToDiskType(diskType)
currentDiskUsage := dn.diskUsages.getOrCreateDisk(dt)
currentDiskUsageMaxVolumeCount := atomic.LoadInt64(&currentDiskUsage.maxVolumeCount)
if currentDiskUsageMaxVolumeCount == int64(maxVolumeCount) {
continue
}
disk := dn.getOrCreateDisk(dt.String())
disk.UpAdjustDiskUsageDelta(dt, &DiskUsageCounts{
maxVolumeCount: int64(maxVolumeCount) - currentDiskUsageMaxVolumeCount,
})
}
}
// AdjustDiskUsageBytes records the physical filesystem capacity a volume server
// reports per disk type, applied as a delta so it flows through the same
// aggregation as the volume counts. Mirrors AdjustMaxVolumeCounts; entries with a
// zero total are treated as "not reported" and skipped.
func (dn *DataNode) AdjustDiskUsageBytes(diskTotalBytes, diskFreeBytes map[string]uint64) {
for diskType, totalBytes := range diskTotalBytes {
// Unlike maxVolumeCount, a 0 here is not "unset" but "not reported": let it
// flow through so a later heartbeat that drops physical-capacity reporting
// (e.g. statfs starts failing) clears the stale bytes and the gate falls
// back to slot-only instead of trusting outdated capacity.
dt := types.ToDiskType(diskType)
currentDiskUsage := dn.diskUsages.getOrCreateDisk(dt)
currentTotal := atomic.LoadInt64(&currentDiskUsage.diskTotalBytes)
currentFree := atomic.LoadInt64(&currentDiskUsage.diskFreeBytes)
newTotal := int64(totalBytes)
newFree := int64(diskFreeBytes[diskType])
if currentTotal == newTotal && currentFree == newFree {
continue
}
disk := dn.getOrCreateDisk(dt.String())
disk.UpAdjustDiskUsageDelta(dt, &DiskUsageCounts{
diskTotalBytes: newTotal - currentTotal,
diskFreeBytes: newFree - currentFree,
})
}
}
// AppendVolumeIds appends the ids of this node's volumes to all, and repeats
// the remote-tier ones on remote, without copying the volume records to read
// them.
func (dn *DataNode) AppendVolumeIds(all, remote, readOnly, readOnlyCanDelete []uint32) ([]uint32, []uint32, []uint32, []uint32) {
dn.RLock()
defer dn.RUnlock()
for _, c := range dn.children {
all, remote, readOnly, readOnlyCanDelete = c.(*Disk).AppendVolumeIds(all, remote, readOnly, readOnlyCanDelete)
}
return all, remote, readOnly, readOnlyCanDelete
}
func (dn *DataNode) GetVolumes() (ret []storage.VolumeInfo) {
dn.RLock()
defer dn.RUnlock()
total := 0
for _, c := range dn.children {
total += c.(*Disk).VolumeCount()
}
ret = make([]storage.VolumeInfo, 0, total)
for _, c := range dn.children {
ret = c.(*Disk).AppendVolumes(ret)
}
return ret
}
// HasDuplicateVolumeIds reports whether the node's last full report named one
// volume id more than once. While it does, the node's digest is not meaningful.
func (dn *DataNode) HasDuplicateVolumeIds() bool {
return dn.duplicateVolumeIds.Load()
}
// VolumeDigest summarises every volume the master believes this node holds. A
// volume server that reports a different digest has drifted from the master and
// needs to resend its volume list.
func (dn *DataNode) VolumeDigest() uint64 {
dn.RLock()
defer dn.RUnlock()
var digest uint64
for _, c := range dn.children {
digest ^= c.(*Disk).VolumeDigest()
}
return digest
}
func (dn *DataNode) GetVolumesById(id needle.VolumeId) (vInfo storage.VolumeInfo, err error) {
dn.RLock()
defer dn.RUnlock()
found := false
for _, c := range dn.children {
disk := c.(*Disk)
vInfo, err = disk.GetVolumesById(id)
if err == nil {
found = true
break
}
}
if found {
return vInfo, nil
} else {
return storage.VolumeInfo{}, fmt.Errorf("volumeInfo not found")
}
}
func (dn *DataNode) GetDataCenter() *DataCenter {
rack := dn.Parent()
if rack == nil {
return nil
}
dcNode := rack.Parent()
if dcNode == nil {
return nil
}
dcValue := dcNode.GetValue()
return dcValue.(*DataCenter)
}
func (dn *DataNode) GetDataCenterId() string {
if dc := dn.GetDataCenter(); dc != nil {
return string(dc.Id())
}
return ""
}
func (dn *DataNode) GetRack() *Rack {
return dn.Parent().(*NodeImpl).value.(*Rack)
}
func (dn *DataNode) GetTopology() *Topology {
p := dn.Parent()
for p.Parent() != nil {
p = p.Parent()
}
t := p.(*Topology)
return t
}
func (dn *DataNode) MatchLocation(ip string, port int) bool {
return dn.Ip == ip && dn.Port == port
}
func (dn *DataNode) Url() string {
return util.JoinHostPort(dn.Ip, dn.Port)
}
func (dn *DataNode) ServerAddress() pb.ServerAddress {
return pb.NewServerAddress(dn.Ip, dn.Port, dn.GrpcPort)
}
type DataNodeInfo struct {
Url string `json:"Url"`
PublicUrl string `json:"PublicUrl"`
Volumes int64 `json:"Volumes"`
EcShards int64 `json:"EcShards"`
Max int64 `json:"Max"`
VolumeIds string `json:"VolumeIds"`
}
func (dn *DataNode) ToInfo() (info DataNodeInfo) {
info.Url = dn.Url()
info.PublicUrl = dn.PublicUrl
// aggregated volume info
var volumeCount, ecShardCount, maxVolumeCount int64
var volumeIds string
for _, diskUsage := range dn.diskUsages.usages {
volumeCount += diskUsage.volumeCount
ecShardCount += diskUsage.ecShardCount
maxVolumeCount += diskUsage.maxVolumeCount
}
for _, disk := range dn.Children() {
d := disk.(*Disk)
volumeIds += " " + d.GetVolumeIds()
}
info.Volumes = volumeCount
info.EcShards = ecShardCount
info.Max = maxVolumeCount
info.VolumeIds = volumeIds
return
}
func (dn *DataNode) ToDataNodeInfo(filter VolumeFilter) *master_pb.DataNodeInfo {
m := &master_pb.DataNodeInfo{
Id: string(dn.Id()),
// Start from disk usage counters so empty disks are still represented
// even when there are no volumes/EC shards on this data node yet.
DiskInfos: dn.diskUsages.ToDiskInfo(),
GrpcPort: uint32(dn.GrpcPort),
Address: dn.Url(), // ip:port for connecting to the volume server
}
if m.DiskInfos == nil {
m.DiskInfos = make(map[string]*master_pb.DiskInfo)
}
for diskType, diskInfo := range m.DiskInfos {
if diskInfo == nil {
m.DiskInfos[diskType] = &master_pb.DiskInfo{Type: diskType}
continue
}
diskInfo.Type = diskType
}
for _, c := range dn.Children() {
disk := c.(*Disk)
m.DiskInfos[string(disk.Id())] = disk.ToDiskInfo(filter)
}
dn.RLock()
metas := make(map[uint32]diskMeta, len(dn.diskMetas))
for diskID, meta := range dn.diskMetas {
metas[diskID] = meta
}
dn.RUnlock()
for _, diskInfo := range m.DiskInfos {
if diskInfo == nil {
continue
}
if meta, found := metas[diskInfo.DiskId]; found {
diskInfo.Tags = append([]string(nil), meta.tags...)
}
// Max per physical disk of this type, empty and unavailable (max 0) ones
// included. Emit only when some disk reports capacity, so an older server
// sending all zeros leaves the map nil and falls back.
diskType := types.ToDiskType(diskInfo.Type)
maxByDisk := make(map[uint32]int64)
anyCapacity := false
for diskID, meta := range metas {
if meta.diskType != diskType {
continue
}
if meta.maxVolumeCount > 0 {
anyCapacity = true
}
maxByDisk[diskID] = meta.maxVolumeCount
}
if anyCapacity {
diskInfo.MaxVolumeCountByDisk = maxByDisk
}
}
return m
}
func (dn *DataNode) UpdateDiskTags(tags []*master_pb.DiskTag) {
if len(tags) == 0 {
return
}
// DiskTags is the full list on each full heartbeat; rebuild fresh to drop
// removed disks.
metas := make(map[uint32]diskMeta, len(tags))
for _, tagInfo := range tags {
if tagInfo == nil {
continue
}
metas[tagInfo.DiskId] = diskMeta{
tags: append([]string(nil), tagInfo.Tags...),
diskType: types.ToDiskType(tagInfo.Type),
maxVolumeCount: tagInfo.MaxVolumeCount,
}
}
dn.Lock()
dn.diskMetas = metas
dn.Unlock()
}

View File

@@ -0,0 +1,199 @@
package topology
import (
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
func (dn *DataNode) GetEcShards() (ret []*erasure_coding.EcVolumeInfo) {
dn.RLock()
for _, c := range dn.children {
disk := c.(*Disk)
ret = append(ret, disk.GetEcShards()...)
}
dn.RUnlock()
return ret
}
// ecShardKey identifies a per-physical-disk EC shard entry on a DataNode.
// A single volume's EC shards can live on multiple physical disks of one
// DataNode (e.g. a 10+4 volume spread across 4 mount points), so entries
// must be tracked per (volume, physical disk) rather than per volume alone.
// See issue #9212.
type ecShardKey struct {
vid needle.VolumeId
diskId types.DiskId
}
func (dn *DataNode) UpdateEcShards(actualShards []*erasure_coding.EcVolumeInfo) (newShards, deletedShards []*erasure_coding.EcVolumeInfo) {
// Aggregate incoming messages by (volume, physical disk). Duplicates for
// the same (vid, diskId) are merged; entries for the same vid on
// different disks stay separate so per-physical-disk attribution is
// preserved all the way to DiskInfo.EcShardInfos — which the admin
// shell's ec.balance / ec.rebuild planners read via eci.DiskId.
actualByKey := make(map[ecShardKey]*erasure_coding.EcVolumeInfo, len(actualShards))
for _, ecShards := range actualShards {
key := ecShardKey{vid: ecShards.VolumeId, diskId: types.DiskId(ecShards.DiskId)}
if existing, ok := actualByKey[key]; ok {
existing.ShardsInfo.Add(ecShards.ShardsInfo)
continue
}
// Clone so subsequent merges for the same key don't mutate the
// caller's EcVolumeInfo, and so the diff below is stable.
clone := *ecShards
clone.ShardsInfo = ecShards.ShardsInfo.Copy()
actualByKey[key] = &clone
}
// Hold dn.Lock for the full read-diff-write cycle so concurrent heartbeats,
// getOrCreateDisk calls, and UpAdjustDiskUsageDelta updates on this data
// node are serialized with us — matching UpdateVolumes' locking model.
// Internal helpers below assume the caller holds dn.Lock.
dn.Lock()
defer dn.Unlock()
existingEcShards := dn.getEcShardsLocked()
// find out the newShards and deletedShards
for _, ecShards := range existingEcShards {
var newShardCount, deletedShardCount int
disk := dn.getOrCreateDisk(ecShards.DiskType)
key := ecShardKey{vid: ecShards.VolumeId, diskId: types.DiskId(ecShards.DiskId)}
if actualEcShards, ok := actualByKey[key]; !ok {
// dn registered ec shards not found in the new set of ec shards
deletedShards = append(deletedShards, ecShards)
deletedShardCount += ecShards.ShardsInfo.Count()
} else {
// found, but maybe the actual shard could be missing
a := actualEcShards.Minus(ecShards)
if a.ShardsInfo.Count() > 0 {
newShards = append(newShards, a)
newShardCount += a.ShardsInfo.Count()
}
d := ecShards.Minus(actualEcShards)
if d.ShardsInfo.Count() > 0 {
deletedShards = append(deletedShards, d)
deletedShardCount += d.ShardsInfo.Count()
}
}
if (newShardCount - deletedShardCount) != 0 {
disk.UpAdjustDiskUsageDelta(types.ToDiskType(ecShards.DiskType), &DiskUsageCounts{
ecShardCount: int64(newShardCount - deletedShardCount),
})
}
}
existingKeys := make(map[ecShardKey]struct{}, len(existingEcShards))
for _, ev := range existingEcShards {
existingKeys[ecShardKey{vid: ev.VolumeId, diskId: types.DiskId(ev.DiskId)}] = struct{}{}
}
for key, ecShards := range actualByKey {
if _, found := existingKeys[key]; found {
continue
}
newShards = append(newShards, ecShards)
disk := dn.getOrCreateDisk(ecShards.DiskType)
disk.UpAdjustDiskUsageDelta(types.ToDiskType(ecShards.DiskType), &DiskUsageCounts{
ecShardCount: int64(ecShards.ShardsInfo.Count()),
})
}
if len(newShards) > 0 || len(deletedShards) > 0 {
// if changed, set to the new ec shard map
dn.doUpdateEcShardsLocked(actualByKey)
}
return
}
// getEcShardsLocked returns the flat list of per-physical-disk EC shard
// entries across all children. Caller MUST hold dn.Lock (or RLock); each
// disk's ecShardsLock is taken internally via Disk.GetEcShards.
func (dn *DataNode) getEcShardsLocked() (ret []*erasure_coding.EcVolumeInfo) {
for _, c := range dn.children {
disk := c.(*Disk)
ret = append(ret, disk.GetEcShards()...)
}
return ret
}
func (dn *DataNode) HasEcShards(volumeId needle.VolumeId) (found bool) {
dn.RLock()
defer dn.RUnlock()
for _, c := range dn.children {
disk := c.(*Disk)
disk.ecShardsLock.RLock()
byDisk, ok := disk.ecShards[volumeId]
has := ok && len(byDisk) > 0
disk.ecShardsLock.RUnlock()
if has {
return true
}
}
return
}
// doUpdateEcShardsLocked rewrites disk.ecShards from actualByKey. Caller
// MUST hold dn.Lock; each disk's ecShardsLock is taken internally around
// the read-modify-write of its ecShards map.
func (dn *DataNode) doUpdateEcShardsLocked(actualByKey map[ecShardKey]*erasure_coding.EcVolumeInfo) {
for _, c := range dn.children {
disk := c.(*Disk)
disk.ecShardsLock.Lock()
disk.ecShards = make(map[needle.VolumeId]map[types.DiskId]*erasure_coding.EcVolumeInfo)
disk.ecShardsLock.Unlock()
}
for _, shard := range actualByKey {
disk := dn.getOrCreateDisk(shard.DiskType)
disk.ecShardsLock.Lock()
byDisk, ok := disk.ecShards[shard.VolumeId]
if !ok {
byDisk = make(map[types.DiskId]*erasure_coding.EcVolumeInfo, 1)
disk.ecShards[shard.VolumeId] = byDisk
}
byDisk[types.DiskId(shard.DiskId)] = shard
disk.ecShardsLock.Unlock()
}
}
func (dn *DataNode) DeltaUpdateEcShards(newShards, deletedShards []*erasure_coding.EcVolumeInfo) {
for _, newShard := range newShards {
dn.AddOrUpdateEcShard(newShard)
}
for _, deletedShard := range deletedShards {
dn.DeleteEcShard(deletedShard)
}
}
func (dn *DataNode) AddOrUpdateEcShard(s *erasure_coding.EcVolumeInfo) {
disk := dn.getOrCreateDisk(s.DiskType)
disk.AddOrUpdateEcShard(s)
}
func (dn *DataNode) DeleteEcShard(s *erasure_coding.EcVolumeInfo) {
disk := dn.getOrCreateDisk(s.DiskType)
disk.DeleteEcShard(s)
}
func (dn *DataNode) HasVolumesById(volumeId needle.VolumeId) (hasVolumeId bool) {
dn.RLock()
defer dn.RUnlock()
for _, c := range dn.children {
disk := c.(*Disk)
if disk.HasVolumesById(volumeId) {
return true
}
}
return false
}

View File

@@ -0,0 +1,177 @@
package topology
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
// TestEcShardsAcrossMultipleDisksOnSameNode reproduces issue #9212.
// When a volume server reports EC shards of the same volume spread across
// multiple physical disks on the same node, the master must register ALL
// shards, not only a subset.
func TestEcShardsAcrossMultipleDisksOnSameNode(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
maxVolumeCounts := map[string]uint32{"": 100}
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", maxVolumeCounts)
const vid = uint32(15)
const collection = "grafana-loki"
const diskType = "" // HDD / default type — all 4 disks share this type
// Volume 15 has its 14 EC shards spread across 3 physical disks on this node.
// Mirrors the "volume-2" row from issue #9212:
// /data1 (diskId 0): ec02, ec06, ec10
// /data2 (diskId 1): ec01, ec04, ec09
// /data4 (diskId 3): ec08, ec12
disk0 := buildEcShardMessage(vid, collection, diskType, 0, []erasure_coding.ShardId{2, 6, 10})
disk1 := buildEcShardMessage(vid, collection, diskType, 1, []erasure_coding.ShardId{1, 4, 9})
disk3 := buildEcShardMessage(vid, collection, diskType, 3, []erasure_coding.ShardId{8, 12})
msgs := []*master_pb.VolumeEcShardInformationMessage{disk0, disk1, disk3}
topo.SyncDataNodeEcShards(msgs, dn)
locs, ok := topo.LookupEcShards(needle.VolumeId(vid))
if !ok {
t.Fatalf("volume %d: no ec shard locations registered at all", vid)
}
// All 8 shards should be visible to the master: 1,2,4,6,8,9,10,12.
wantShards := []erasure_coding.ShardId{1, 2, 4, 6, 8, 9, 10, 12}
var gotShards []erasure_coding.ShardId
for shardId, dataNodes := range locs.Locations {
if len(dataNodes) > 0 {
gotShards = append(gotShards, erasure_coding.ShardId(shardId))
}
}
if len(gotShards) != len(wantShards) {
t.Errorf("volume %d: topology.LookupEcShards sees %d shards %v, want %d shards %v",
vid, len(gotShards), gotShards, len(wantShards), wantShards)
}
for _, want := range wantShards {
if len(locs.Locations[want]) == 0 {
t.Errorf("volume %d: shard %d missing from topology (bug #9212)", vid, want)
}
}
// The DataNode's own view (what drives volume.list output, admin UI, and
// ec.rebuild dry-run diagnostics) must also see all shards across all disks.
dnShards := dn.GetEcShards()
dnShardCount := 0
dnShardBitmap := erasure_coding.ShardBits(0)
for _, ev := range dnShards {
if ev.VolumeId == needle.VolumeId(vid) {
dnShardCount += ev.ShardsInfo.Count()
dnShardBitmap |= erasure_coding.ShardBits(ev.ShardsInfo.Bitmap())
}
}
if dnShardCount != len(wantShards) {
t.Errorf("volume %d: DataNode.GetEcShards reports %d shards (bitmap=0b%b), want %d shards %v (bug #9212)",
vid, dnShardCount, dnShardBitmap, len(wantShards), wantShards)
}
// Per-physical-disk attribution must survive all the way to the protobuf
// DiskInfo.EcShardInfos consumed by ec.balance / ec.rebuild planners. The
// admin shell groups shards by eci.DiskId, so each physical disk needs a
// separate message with its own shard subset.
wantPerDisk := map[uint32][]erasure_coding.ShardId{
0: {2, 6, 10},
1: {1, 4, 9},
3: {8, 12},
}
dnInfo := dn.ToDataNodeInfo(VolumeFilter{})
gotPerDisk := map[uint32][]erasure_coding.ShardId{}
for _, diskInfo := range dnInfo.DiskInfos {
for _, eci := range diskInfo.EcShardInfos {
if eci.Id != vid {
continue
}
si := erasure_coding.ShardsInfoFromVolumeEcShardInformationMessage(eci)
gotPerDisk[eci.DiskId] = append(gotPerDisk[eci.DiskId], si.Ids()...)
}
}
for diskId, want := range wantPerDisk {
got := gotPerDisk[diskId]
if !shardSetEqual(got, want) {
t.Errorf("volume %d diskId %d: DiskInfo.EcShardInfos report shards %v, want %v (per-physical-disk attribution lost)",
vid, diskId, got, want)
}
}
for diskId := range gotPerDisk {
if _, ok := wantPerDisk[diskId]; !ok {
t.Errorf("volume %d: unexpected diskId %d in DiskInfo.EcShardInfos, shards=%v",
vid, diskId, gotPerDisk[diskId])
}
}
}
func shardSetEqual(a, b []erasure_coding.ShardId) bool {
if len(a) != len(b) {
return false
}
seen := make(map[erasure_coding.ShardId]int, len(a))
for _, id := range a {
seen[id]++
}
for _, id := range b {
seen[id]--
if seen[id] < 0 {
return false
}
}
return true
}
// TestEcShardsAfterRestartHeartbeat simulates the exact issue #9212 flow:
// the volume server starts up, loads shards from each disk, and sends a
// single full-sync heartbeat containing one VolumeEcShardInformationMessage
// per (disk, volume). The master must end up with all shards visible per
// DataNode, not just the subset belonging to whichever disk happened to be
// iterated last.
func TestEcShardsAfterRestartHeartbeat(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 100})
// Matches "volume-1" from the bug report: ec13 on /data1, ec03 on /data3.
msgs := []*master_pb.VolumeEcShardInformationMessage{
buildEcShardMessage(15, "grafana-loki", "", 0, []erasure_coding.ShardId{13}),
buildEcShardMessage(15, "grafana-loki", "", 2, []erasure_coding.ShardId{3}),
}
topo.SyncDataNodeEcShards(msgs, dn)
dnShards := dn.GetEcShards()
var combined erasure_coding.ShardBits
for _, ev := range dnShards {
if ev.VolumeId == 15 {
combined |= erasure_coding.ShardBits(ev.ShardsInfo.Bitmap())
}
}
if combined.Count() != 2 {
t.Errorf("volume 15: DataNode sees %d shards (bitmap=0b%b) after full sync of 2 disks; want both shards 3 and 13 visible (bug #9212)",
combined.Count(), combined)
}
}
func buildEcShardMessage(vid uint32, collection, diskType string, diskId uint32, shardIds []erasure_coding.ShardId) *master_pb.VolumeEcShardInformationMessage {
si := erasure_coding.NewShardsInfo()
for _, sid := range shardIds {
si.Set(erasure_coding.NewShardInfo(sid, erasure_coding.ShardSize(1024)))
}
return &master_pb.VolumeEcShardInformationMessage{
Id: vid,
Collection: collection,
DiskType: diskType,
DiskId: diskId,
EcIndexBits: si.Bitmap(),
ShardSizes: si.SizesInt64(),
}
}

View File

@@ -0,0 +1,88 @@
package topology
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/sequence"
)
// A disk with no volumes/shards surfaces via DiskTags.
func TestToDataNodeInfoReportsEmptyPhysicalDisks(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 1000})
dn.AdjustMaxVolumeCounts(map[string]uint32{"": 1000})
dn.UpdateDiskTags([]*master_pb.DiskTag{
{DiskId: 0, Type: "", MaxVolumeCount: 350},
{DiskId: 1, Type: "", MaxVolumeCount: 350},
{DiskId: 2, Type: "", MaxVolumeCount: 300}, // empty disk, never held a volume
})
info := dn.ToDataNodeInfo(VolumeFilter{})
di, ok := info.DiskInfos[""]
if !ok {
t.Fatalf("missing HDD disk info")
}
if len(di.MaxVolumeCountByDisk) != 3 {
t.Fatalf("want 3 physical disks reported, got %d", len(di.MaxVolumeCountByDisk))
}
split := di.SplitByPhysicalDisk()
if len(split) != 3 {
t.Fatalf("SplitByPhysicalDisk: want 3 disks including the empty one, got %d", len(split))
}
byID := map[uint32]*master_pb.DiskInfo{}
for _, d := range split {
byID[d.DiskId] = d
}
if byID[2] == nil {
t.Fatalf("empty disk 2 not surfaced")
}
if byID[2].MaxVolumeCount != 300 || byID[2].VolumeCount != 0 {
t.Errorf("empty disk 2: want max 300 / 0 volumes, got max %d / %d volumes",
byID[2].MaxVolumeCount, byID[2].VolumeCount)
}
}
// A max-0 (unavailable) disk stays listed when the node reports capacity.
func TestToDataNodeInfoKeepsZeroCapacityDisk(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 700})
dn.AdjustMaxVolumeCounts(map[string]uint32{"": 700})
dn.UpdateDiskTags([]*master_pb.DiskTag{
{DiskId: 0, Type: "", MaxVolumeCount: 350},
{DiskId: 1, Type: "", MaxVolumeCount: 350},
{DiskId: 2, Type: "", MaxVolumeCount: 0}, // unavailable disk
})
di := dn.ToDataNodeInfo(VolumeFilter{}).DiskInfos[""]
if len(di.MaxVolumeCountByDisk) != 3 {
t.Fatalf("want 3 physical disks (incl the zero-capacity one), got %d", len(di.MaxVolumeCountByDisk))
}
}
// An older server sending no per-disk capacity leaves the map empty.
func TestToDataNodeInfoFallsBackWhenNoCapacityReported(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 700})
dn.AdjustMaxVolumeCounts(map[string]uint32{"": 700})
// Older server: DiskTags carry disk_id (+tags) but no type/max.
dn.UpdateDiskTags([]*master_pb.DiskTag{
{DiskId: 0},
{DiskId: 1},
})
di := dn.ToDataNodeInfo(VolumeFilter{}).DiskInfos[""]
if len(di.MaxVolumeCountByDisk) != 0 {
t.Fatalf("want no per-disk max (fallback), got %d", len(di.MaxVolumeCountByDisk))
}
}

550
weed/topology/disk.go Normal file
View File

@@ -0,0 +1,550 @@
package topology
import (
"fmt"
"slices"
"sync"
"sync/atomic"
"time"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage"
)
type Disk struct {
NodeImpl
volumes map[needle.VolumeId]*storage.VolumeInfo
// ecShards is nested so the same volume can retain separate entries per
// physical disk id. A single topology Disk represents one DiskType on a
// DataNode and may front multiple physical disks of that type, so EC
// shards of one volume can legitimately live on several of them. The
// outer key is the volume id; the inner key is the physical disk id.
ecShards map[needle.VolumeId]map[types.DiskId]*erasure_coding.EcVolumeInfo
ecShardsLock sync.RWMutex
// volumeDigest is the xor of every volume's ReportHash. Order-independent
// and its own inverse, so it stays current by xoring a volume out before
// its old state is dropped and back in after the new one lands.
volumeDigest uint64
// volumeIdDigest covers which volumes are on the disk, ignoring their
// state, so it can be compared against the lookup index the master serves
// reads from. The two indexes are maintained separately and have been seen
// to drift.
volumeIdDigest uint64
// volumeAddedAt remembers when each volume reached this view of the disk
// without a server report having confirmed it yet. Registration by the
// master itself -- volume growth -- races the heartbeat in flight, which
// cannot name a volume created after it was collected.
volumeAddedAt map[needle.VolumeId]time.Time
}
// volumeRemovalGracePeriod is how long an unconfirmed volume survives a report
// that does not name it. Removing a just-grown volume strands its collection
// without writable volumes, so the report that raced the grow does not get to
// erase it; the cap keeps a registration that never materializes server-side
// from lingering forever.
const volumeRemovalGracePeriod = 10 * time.Second
func NewDisk(diskType string) *Disk {
s := &Disk{}
s.id = NodeId(diskType)
s.nodeType = "Disk"
s.diskUsages = newDiskUsages()
s.volumes = make(map[needle.VolumeId]*storage.VolumeInfo, 2)
s.volumeAddedAt = make(map[needle.VolumeId]time.Time, 2)
s.ecShards = make(map[needle.VolumeId]map[types.DiskId]*erasure_coding.EcVolumeInfo, 2)
s.NodeImpl.value = s
return s
}
type DiskUsages struct {
sync.RWMutex
usages map[types.DiskType]*DiskUsageCounts
}
func newDiskUsages() *DiskUsages {
return &DiskUsages{
usages: make(map[types.DiskType]*DiskUsageCounts),
}
}
func (d *DiskUsages) negative() *DiskUsages {
d.RLock()
defer d.RUnlock()
t := newDiskUsages()
for diskType, b := range d.usages {
a := t.getOrCreateDisk(diskType)
a.volumeCount = -b.volumeCount
a.remoteVolumeCount = -b.remoteVolumeCount
a.activeVolumeCount = -b.activeVolumeCount
a.ecShardCount = -b.ecShardCount
a.maxVolumeCount = -b.maxVolumeCount
a.diskTotalBytes = -b.diskTotalBytes
a.diskFreeBytes = -b.diskFreeBytes
}
return t
}
func (d *DiskUsages) ToDiskInfo() map[string]*master_pb.DiskInfo {
d.RLock()
defer d.RUnlock()
ret := make(map[string]*master_pb.DiskInfo)
for diskType, diskUsageCounts := range d.usages {
usage := diskUsageCounts.snapshot()
m := &master_pb.DiskInfo{
VolumeCount: usage.volumeCount,
MaxVolumeCount: usage.maxVolumeCount,
FreeVolumeCount: usage.maxVolumeCount - (usage.volumeCount - usage.remoteVolumeCount) - erasure_coding.VolumeSlots(usage.ecShardCount),
ActiveVolumeCount: usage.activeVolumeCount,
RemoteVolumeCount: usage.remoteVolumeCount,
DiskTotalBytes: uint64(max(0, usage.diskTotalBytes)),
DiskFreeBytes: uint64(max(0, usage.diskFreeBytes)),
}
ret[string(diskType)] = m
}
return ret
}
func (d *DiskUsages) FreeSpace() (freeSpace int64) {
d.RLock()
defer d.RUnlock()
for _, diskUsage := range d.usages {
freeSpace += diskUsage.FreeSpace()
}
return
}
func (d *DiskUsages) GetMaxVolumeCount() (maxVolumeCount int64) {
d.RLock()
defer d.RUnlock()
for _, diskUsage := range d.usages {
maxVolumeCount += diskUsage.maxVolumeCount
}
return
}
// FreeBytes sums the space one volume server reports as still free on its
// filesystems. reported is false as soon as a disk holding volume slots says
// nothing -- a volume server older than the field looks that way -- since
// leaving its space out would understate the room the server has.
func (d *DiskUsages) FreeBytes() (freeBytes uint64, reported bool) {
d.RLock()
defer d.RUnlock()
for _, diskUsageCounts := range d.usages {
usage := diskUsageCounts.snapshot()
if usage.diskTotalBytes <= 0 {
if usage.maxVolumeCount > 0 {
return 0, false
}
continue
}
freeBytes += uint64(max(0, usage.diskFreeBytes))
}
return freeBytes, true
}
type DiskUsageCounts struct {
volumeCount int64
remoteVolumeCount int64
activeVolumeCount int64
ecShardCount int64
maxVolumeCount int64
// Physical filesystem capacity reported by the volume server, in bytes.
// 0 means the volume server did not report it (e.g. an older build).
diskTotalBytes int64
diskFreeBytes int64
}
func (a *DiskUsageCounts) addDiskUsageCounts(b *DiskUsageCounts) {
atomic.AddInt64(&a.volumeCount, b.volumeCount)
atomic.AddInt64(&a.remoteVolumeCount, b.remoteVolumeCount)
atomic.AddInt64(&a.activeVolumeCount, b.activeVolumeCount)
atomic.AddInt64(&a.ecShardCount, b.ecShardCount)
atomic.AddInt64(&a.maxVolumeCount, b.maxVolumeCount)
atomic.AddInt64(&a.diskTotalBytes, b.diskTotalBytes)
atomic.AddInt64(&a.diskFreeBytes, b.diskFreeBytes)
}
// snapshot reads each counter atomically, so a reader sees whole values rather
// than ones a concurrent heartbeat is halfway through writing. They are still
// read one at a time, so they need not all describe the same instant.
func (a *DiskUsageCounts) snapshot() DiskUsageCounts {
return DiskUsageCounts{
volumeCount: atomic.LoadInt64(&a.volumeCount),
remoteVolumeCount: atomic.LoadInt64(&a.remoteVolumeCount),
activeVolumeCount: atomic.LoadInt64(&a.activeVolumeCount),
ecShardCount: atomic.LoadInt64(&a.ecShardCount),
maxVolumeCount: atomic.LoadInt64(&a.maxVolumeCount),
diskTotalBytes: atomic.LoadInt64(&a.diskTotalBytes),
diskFreeBytes: atomic.LoadInt64(&a.diskFreeBytes),
}
}
func (a *DiskUsageCounts) FreeSpace() int64 {
u := a.snapshot()
return u.maxVolumeCount + u.remoteVolumeCount - u.volumeCount - erasure_coding.VolumeSlots(u.ecShardCount)
}
func (du *DiskUsages) getOrCreateDisk(diskType types.DiskType) *DiskUsageCounts {
du.Lock()
defer du.Unlock()
t, found := du.usages[diskType]
if found {
return t
}
t = &DiskUsageCounts{}
du.usages[diskType] = t
return t
}
func (d *Disk) String() string {
d.RLock()
defer d.RUnlock()
return fmt.Sprintf("Disk:%s, volumes:%v, ecShards:%v", d.NodeImpl.String(), d.volumes, d.ecShards)
}
func (d *Disk) AddOrUpdateVolume(v storage.VolumeInfo) (isNew, isChanged, tierTransition bool) {
d.Lock()
defer d.Unlock()
return d.doAddOrUpdateVolume(v, true)
}
// AddProvisionalVolume records a volume the master registered on its own --
// volume growth -- before any server report has named it. Until one does, the
// volume is protected from removal by a report that raced its creation.
func (d *Disk) AddProvisionalVolume(v storage.VolumeInfo) (isNew, isChanged, tierTransition bool) {
d.Lock()
defer d.Unlock()
return d.doAddOrUpdateVolume(v, false)
}
// SetVolumeReadOnly records the read-only flag the server reported for vid,
// keeping the report digest and the active volume count in step, and reports
// whether the disk holds vid. It is not a volume report: a provisional volume
// stays protected from a stale report until one names it.
func (d *Disk) SetVolumeReadOnly(vid needle.VolumeId, readOnly bool) (found bool) {
d.Lock()
defer d.Unlock()
v, found := d.volumes[vid]
if !found || v.ReadOnly == readOnly {
return found
}
d.volumeDigest ^= v.ReportHash()
v.ReadOnly = readOnly
d.volumeDigest ^= v.ReportHash()
delta := &DiskUsageCounts{activeVolumeCount: 1}
if readOnly {
delta.activeVolumeCount = -1
}
d.UpAdjustDiskUsageDelta(types.ToDiskType(v.DiskType), delta)
return true
}
// doAddOrUpdateVolume returns three signals about how v was installed against
// any existing record:
//
// - isNew: no record was held before; the volume arrived.
// - isChanged: the ReadOnly flag flipped (the only field isChanged currently
// tracks). Other fields changing without ReadOnly flipping leaves this
// false.
// - tierTransition: v's IsRemote() classification differs from the previous
// record. The volume was already known and remains servable, but every
// connected client needs to refresh its replica priority -- a remote-tier
// replica restored locally must jump the read order, and one newly tiered
// to remote storage must give way. Callers that broadcast volume changes
// to clients must include tier-transitioned volumes alongside arrivals.
func (d *Disk) doAddOrUpdateVolume(v storage.VolumeInfo, fromReport bool) (isNew, isChanged, tierTransition bool) {
deltaDiskUsage := &DiskUsageCounts{}
if oldV, ok := d.volumes[v.Id]; !ok {
stored := v
d.volumes[v.Id] = &stored
if !fromReport {
d.volumeAddedAt[v.Id] = time.Now()
}
d.volumeDigest ^= v.ReportHash()
d.volumeIdDigest ^= VolumeIdDigestHash(v.Id)
deltaDiskUsage.volumeCount = 1
if v.IsRemote() {
deltaDiskUsage.remoteVolumeCount = 1
}
if !v.ReadOnly {
deltaDiskUsage.activeVolumeCount = 1
}
d.UpAdjustMaxVolumeId(v.Id)
d.UpAdjustDiskUsageDelta(types.ToDiskType(v.DiskType), deltaDiskUsage)
isNew = true
} else {
if !fromReport && v.DiskId == 0 && oldV.DiskId != 0 {
// A provisional (grow-time) record carries no disk id -- the
// master cannot know which directory the server chose. Keep the
// one the server's report already named, before the digest below
// is computed, or the stored record would drift from what the
// server keeps reporting.
v.DiskId = oldV.DiskId
}
tierTransition = oldV.IsRemote() != v.IsRemote()
if tierTransition {
if v.IsRemote() {
deltaDiskUsage.remoteVolumeCount = 1
}
if oldV.IsRemote() {
deltaDiskUsage.remoteVolumeCount = -1
}
d.UpAdjustDiskUsageDelta(types.ToDiskType(v.DiskType), deltaDiskUsage)
}
d.volumeDigest ^= oldV.ReportHash() ^ v.ReportHash()
if fromReport {
delete(d.volumeAddedAt, v.Id)
}
isChanged = oldV.ReadOnly != v.ReadOnly || oldV.ReadOnlyCanDelete != v.ReadOnlyCanDelete
if oldV.ReadOnly != v.ReadOnly {
// Adjust active volume count when ReadOnly status changes
// Use a separate delta object to avoid affecting other metric adjustments
readOnlyDelta := &DiskUsageCounts{}
if v.ReadOnly {
// Changed from writable to read-only
readOnlyDelta.activeVolumeCount = -1
} else {
// Changed from read-only to writable
readOnlyDelta.activeVolumeCount = 1
}
d.UpAdjustDiskUsageDelta(types.ToDiskType(v.DiskType), readOnlyDelta)
}
// Written through the pointer the map already holds, and only after
// everything above has read the old value off it.
*oldV = v
}
return
}
func (d *Disk) GetVolumes() []storage.VolumeInfo {
return d.AppendVolumes(make([]storage.VolumeInfo, 0, d.VolumeCount()))
}
// AppendVolumeIds appends the ids of the disk's volumes to all, and repeats
// the remote-tier ones on remote. Callers that only need to name volumes use
// this rather than AppendVolumes, which copies a whole record per volume to
// be read for four bytes of it.
func (d *Disk) AppendVolumeIds(all, remote, readOnly, readOnlyCanDelete []uint32) ([]uint32, []uint32, []uint32, []uint32) {
d.RLock()
defer d.RUnlock()
for id, v := range d.volumes {
all = append(all, uint32(id))
if v.IsRemote() {
remote = append(remote, uint32(id))
}
if v.ReadOnly {
readOnly = append(readOnly, uint32(id))
if v.ReadOnlyCanDelete {
readOnlyCanDelete = append(readOnlyCanDelete, uint32(id))
}
}
}
return all, remote, readOnly, readOnlyCanDelete
}
// AppendVolumes appends the disk's volumes to dst, so a caller gathering
// several disks fills one slice instead of concatenating a copy per disk.
func (d *Disk) AppendVolumes(dst []storage.VolumeInfo) []storage.VolumeInfo {
d.RLock()
defer d.RUnlock()
for _, v := range d.volumes {
dst = append(dst, *v)
}
return dst
}
func (d *Disk) VolumeCount() int {
d.RLock()
defer d.RUnlock()
return len(d.volumes)
}
// RemoveVolumesNotIn drops the volumes the heartbeat did not name on this disk
// and returns them, so a heartbeat can be diffed without copying the volume map
// out. A volume named on another disk has moved, and counts as absent here.
func (d *Disk) RemoveVolumesNotIn(reported *reportedVolumes) (removed []storage.VolumeInfo) {
diskTypeIndex := reported.diskTypeIndex(string(d.Id()))
d.Lock()
defer d.Unlock()
now := time.Now()
for vid, v := range d.volumes {
if reported.namedOn(vid, diskTypeIndex) {
// The server confirmed this volume; from here on its absence from
// a report is meaningful.
delete(d.volumeAddedAt, vid)
continue
}
// A volume the master registered itself and no report has confirmed
// yet is likely racing the list being applied, which was collected
// before the grow finished. Explicitly reported deletions still
// remove immediately through DeleteVolumeById.
if addedAt, unconfirmed := d.volumeAddedAt[vid]; unconfirmed && now.Sub(addedAt) < volumeRemovalGracePeriod {
continue
}
removed = append(removed, *v)
delete(d.volumes, vid)
delete(d.volumeAddedAt, vid)
d.volumeDigest ^= v.ReportHash()
d.volumeIdDigest ^= VolumeIdDigestHash(vid)
}
return removed
}
func (d *Disk) GetVolumesById(id needle.VolumeId) (storage.VolumeInfo, error) {
d.RLock()
defer d.RUnlock()
vInfo, ok := d.volumes[id]
if ok {
return *vInfo, nil
} else {
return storage.VolumeInfo{}, fmt.Errorf("volumeInfo not found")
}
}
func (d *Disk) DeleteVolumeById(id needle.VolumeId) {
d.Lock()
defer d.Unlock()
if v, ok := d.volumes[id]; ok {
d.volumeDigest ^= v.ReportHash()
d.volumeIdDigest ^= VolumeIdDigestHash(id)
delete(d.volumes, id)
delete(d.volumeAddedAt, id)
}
}
// VolumeDigest returns the disk's running volume digest.
func (d *Disk) VolumeDigest() uint64 {
d.RLock()
defer d.RUnlock()
return d.volumeDigest
}
// VolumeIdDigest returns the digest of which volumes the disk holds.
func (d *Disk) VolumeIdDigest() uint64 {
d.RLock()
defer d.RUnlock()
return d.volumeIdDigest
}
func (d *Disk) GetDataCenter() *DataCenter {
dn := d.Parent()
rack := dn.Parent()
dcNode := rack.Parent()
dcValue := dcNode.GetValue()
return dcValue.(*DataCenter)
}
func (d *Disk) GetRack() *Rack {
return d.Parent().Parent().(*NodeImpl).value.(*Rack)
}
func (d *Disk) GetTopology() *Topology {
p := d.Parent()
for p.Parent() != nil {
p = p.Parent()
}
t := p.(*Topology)
return t
}
func (d *Disk) ToMap() interface{} {
ret := make(map[string]interface{})
diskUsage := d.diskUsages.getOrCreateDisk(types.ToDiskType(string(d.Id())))
ret["Volumes"] = diskUsage.volumeCount
ret["VolumeIds"] = d.GetVolumeIds()
ret["EcShards"] = diskUsage.ecShardCount
ret["Max"] = diskUsage.maxVolumeCount
ret["Free"] = d.FreeSpace()
return ret
}
func (d *Disk) FreeSpace() int64 {
t := d.diskUsages.getOrCreateDisk(types.ToDiskType(string(d.Id())))
return t.FreeSpace()
}
func (d *Disk) ToDiskInfo(filter VolumeFilter) *master_pb.DiskInfo {
diskUsage := d.diskUsages.getOrCreateDisk(types.ToDiskType(string(d.Id()))).snapshot()
// Built under the read lock rather than from a copy as large as the
// messages it fed. Nothing here re-enters the topology, so the hold is safe.
d.RLock()
// Reserving room for every volume would keep what a filter set out not to
// build.
capacity := 0
if filter.SelectsEverything() {
capacity = len(d.volumes)
}
volumeInfos := make([]*master_pb.VolumeInformationMessage, 0, capacity)
var diskId uint32
var haveDiskId bool
for _, v := range d.volumes {
// Any volume names the disk, including one filtered out. The smallest
// rather than whichever the map yields first, so that two listings of
// an unchanged disk agree when it fronts several physical disks.
if !haveDiskId || v.DiskId < diskId {
diskId, haveDiskId = v.DiskId, true
}
if !filter.matches(v) {
continue
}
volumeInfos = append(volumeInfos, v.ToVolumeInformationMessage())
}
d.RUnlock()
ecShards := d.GetEcShards()
if !haveDiskId {
for _, ecv := range ecShards {
if !haveDiskId || ecv.DiskId < diskId {
diskId, haveDiskId = ecv.DiskId, true
}
}
}
m := &master_pb.DiskInfo{
Type: string(d.Id()),
VolumeCount: diskUsage.volumeCount,
MaxVolumeCount: diskUsage.maxVolumeCount,
FreeVolumeCount: diskUsage.maxVolumeCount - (diskUsage.volumeCount - diskUsage.remoteVolumeCount) - erasure_coding.VolumeSlots(diskUsage.ecShardCount),
ActiveVolumeCount: diskUsage.activeVolumeCount,
RemoteVolumeCount: diskUsage.remoteVolumeCount,
DiskId: diskId,
DiskTotalBytes: uint64(max(0, diskUsage.diskTotalBytes)),
DiskFreeBytes: uint64(max(0, diskUsage.diskFreeBytes)),
}
m.VolumeInfos = volumeInfos
ecCapacity := 0
if filter.SelectsEverything() {
ecCapacity = len(ecShards)
}
m.EcShardInfos = make([]*master_pb.VolumeEcShardInformationMessage, 0, ecCapacity)
for _, ecv := range ecShards {
if !filter.matches(ecv) {
continue
}
m.EcShardInfos = append(m.EcShardInfos, ecv.ToVolumeEcShardInformationMessage())
}
return m
}
// GetVolumeIds returns the human readable volume ids limited to count of max 100.
func (d *Disk) GetVolumeIds() string {
d.RLock()
defer d.RUnlock()
ids := make([]int, 0, len(d.volumes))
for k := range d.volumes {
ids = append(ids, int(k))
}
slices.Sort(ids)
return util.HumanReadableIntsMax(100, ids...)
}

106
weed/topology/disk_ec.go Normal file
View File

@@ -0,0 +1,106 @@
package topology
import (
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
func (d *Disk) GetEcShards() (ret []*erasure_coding.EcVolumeInfo) {
d.ecShardsLock.RLock()
defer d.ecShardsLock.RUnlock()
// Total entries = sum of per-volume per-disk entries, which typically
// exceeds the number of unique volumes once shards span multiple disks.
total := 0
for _, byDisk := range d.ecShards {
total += len(byDisk)
}
ret = make([]*erasure_coding.EcVolumeInfo, 0, total)
for _, byDisk := range d.ecShards {
for _, ecVolumeInfo := range byDisk {
ret = append(ret, ecVolumeInfo)
}
}
return ret
}
func (d *Disk) AddOrUpdateEcShard(s *erasure_coding.EcVolumeInfo) {
d.ecShardsLock.Lock()
defer d.ecShardsLock.Unlock()
byDisk, ok := d.ecShards[s.VolumeId]
if !ok {
byDisk = make(map[types.DiskId]*erasure_coding.EcVolumeInfo, 1)
d.ecShards[s.VolumeId] = byDisk
}
diskId := types.DiskId(s.DiskId)
delta := 0
if existing, ok := byDisk[diskId]; !ok {
byDisk[diskId] = s
delta = s.ShardsInfo.Count()
} else {
oldCount := existing.ShardsInfo.Count()
existing.ShardsInfo.Add(s.ShardsInfo)
delta = existing.ShardsInfo.Count() - oldCount
}
if delta != 0 {
d.UpAdjustDiskUsageDelta(types.ToDiskType(string(d.Id())), &DiskUsageCounts{
ecShardCount: int64(delta),
})
}
}
func (d *Disk) DeleteEcShard(s *erasure_coding.EcVolumeInfo) {
d.ecShardsLock.Lock()
defer d.ecShardsLock.Unlock()
byDisk, ok := d.ecShards[s.VolumeId]
if !ok {
return
}
diskId := types.DiskId(s.DiskId)
existing, ok := byDisk[diskId]
if !ok {
return
}
oldCount := existing.ShardsInfo.Count()
existing.ShardsInfo.Subtract(s.ShardsInfo)
delta := existing.ShardsInfo.Count() - oldCount
if delta != 0 {
d.UpAdjustDiskUsageDelta(types.ToDiskType(string(d.Id())), &DiskUsageCounts{
ecShardCount: int64(delta),
})
}
if existing.ShardsInfo.Count() == 0 {
delete(byDisk, diskId)
if len(byDisk) == 0 {
delete(d.ecShards, s.VolumeId)
}
}
}
func (d *Disk) HasVolumesById(id needle.VolumeId) (hasVolumeId bool) {
// check whether normal volumes has this volume id
d.RLock()
_, ok := d.volumes[id]
if ok {
hasVolumeId = true
}
d.RUnlock()
if hasVolumeId {
return
}
// check whether ec shards has this volume id
d.ecShardsLock.RLock()
if byDisk, ok := d.ecShards[id]; ok && len(byDisk) > 0 {
hasVolumeId = true
}
d.ecShardsLock.RUnlock()
return
}

View File

@@ -0,0 +1,69 @@
package topology
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
)
func TestDiskInfoReportsEveryVolume(t *testing.T) {
topo := NewTopology("diskinfo", nil, 32*1024*1024*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 100})
volumes := []*master_pb.VolumeInformationMessage{
{Id: 1, Collection: "c", Size: 1000, FileCount: 10, Version: 3, DiskId: 2},
{Id: 2, Collection: "c", Size: 2000, FileCount: 20, Version: 3, DiskId: 2},
{Id: 3, Collection: "other", Size: 3000, FileCount: 30, Version: 3, DiskId: 2},
}
topo.SyncDataNodeRegistration(volumes, dn)
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 9, Collection: "c", EcIndexBits: 0x3fff, DiskId: 2},
}, dn)
var info *master_pb.DiskInfo
for _, c := range dn.Children() {
info = c.(*Disk).ToDiskInfo(VolumeFilter{})
}
if info == nil {
t.Fatal("the node reported no disk")
}
if len(info.VolumeInfos) != len(volumes) {
t.Fatalf("reported %d volumes, want %d", len(info.VolumeInfos), len(volumes))
}
if len(info.EcShardInfos) != 1 {
t.Fatalf("reported %d ec volumes, want 1", len(info.EcShardInfos))
}
if info.DiskId != 2 {
t.Errorf("reported disk id %d, want the one its volumes are on", info.DiskId)
}
bySize := map[uint32]uint64{}
for _, v := range info.VolumeInfos {
bySize[v.Id] = v.Size
}
for _, want := range volumes {
if got := bySize[want.Id]; got != want.Size {
t.Errorf("volume %d reported size %d, want %d", want.Id, got, want.Size)
}
}
}
func TestDiskInfoReportsTheDiskIdOfEcOnlyDisks(t *testing.T) {
topo := NewTopology("diskinfo", nil, 32*1024*1024*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 100})
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 9, Collection: "c", EcIndexBits: 0x3fff, DiskId: 5},
}, dn)
for _, c := range dn.Children() {
info := c.(*Disk).ToDiskInfo(VolumeFilter{})
if len(info.VolumeInfos) != 0 {
t.Fatalf("expected no regular volumes, got %d", len(info.VolumeInfos))
}
if info.DiskId != 5 {
t.Errorf("reported disk id %d, want the one its shards are on", info.DiskId)
}
}
}

View File

@@ -0,0 +1,68 @@
package topology
import (
"sync/atomic"
"testing"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
// A provisional (grow-time) registration carries no disk id; when it lands
// after the server's own report -- the server pushes its report during the
// AllocateVolume RPC, so either order happens -- it must not erase the disk id
// the report recorded.
func TestProvisionalUpdateKeepsReportedDiskId(t *testing.T) {
disk := NewDisk(types.HardDriveType.String())
disk.AddOrUpdateVolume(storage.VolumeInfo{Id: 1, DiskId: 2})
disk.AddProvisionalVolume(storage.VolumeInfo{Id: 1})
v, err := disk.GetVolumesById(1)
if err != nil {
t.Fatal(err)
}
if v.DiskId != 2 {
t.Fatalf("DiskId = %d, want 2 (provisional update clobbered the reported value)", v.DiskId)
}
// A later server report naming a different disk still wins.
disk.AddOrUpdateVolume(storage.VolumeInfo{Id: 1, DiskId: 1})
if v, _ = disk.GetVolumesById(1); v.DiskId != 1 {
t.Fatalf("DiskId = %d, want 1 (a real report must override)", v.DiskId)
}
}
// A read-only mark from the server is not a volume report: it must not end the
// grace period that keeps a just-grown volume safe from a full report collected
// before the grow, while still keeping the digest and active count in step.
func TestSetVolumeReadOnlyKeepsProvisionalProtection(t *testing.T) {
dn := NewDataNode("dn1")
vi := storage.VolumeInfo{Id: 1, DiskType: types.HardDriveType.String()}
dn.AddProvisionalVolume(vi)
dn.SetVolumeReadOnly(1, true)
readOnly := vi
readOnly.ReadOnly = true
if got, want := dn.VolumeDigest(), readOnly.ReportHash(); got != want {
t.Errorf("digest = %x, want %x, the hash of the volume as the server will report it", got, want)
}
if got := atomic.LoadInt64(&dn.GetDiskUsages().getOrCreateDisk(types.HardDriveType).activeVolumeCount); got != 0 {
t.Errorf("activeVolumeCount = %d, want 0 after the read-only mark", got)
}
// the stale report, collected before the grow, does not name the volume
if _, deleted, _ := dn.UpdateVolumes(nil); len(deleted) != 0 {
t.Fatalf("a report that raced the grow removed the volume: %v", deleted)
}
if v, err := dn.GetVolumesById(1); err != nil || !v.ReadOnly {
t.Fatalf("GetVolumesById = %+v, %v; want the volume kept, read-only", v, err)
}
// the server's own report confirms it, and from then on absence counts
dn.UpdateVolumes([]storage.VolumeInfo{readOnly})
if _, deleted, _ := dn.UpdateVolumes(nil); len(deleted) != 1 {
t.Fatalf("a report after confirmation should remove the volume, got %v", deleted)
}
}

View File

@@ -0,0 +1,331 @@
package topology
import (
"slices"
"testing"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
// addMaintenanceTestNode links a data node with maxVol free slots under rack.
func addMaintenanceTestNode(rack *Rack, id string, maxVol int64) *DataNode {
dn := NewDataNode(id)
dn.Ip = id
rack.LinkChildNode(dn)
dn.getOrCreateDisk("").UpAdjustDiskUsageDelta("", &DiskUsageCounts{maxVolumeCount: maxVol})
return dn
}
func newMaintenanceTestTopology() (*Topology, *DataCenter) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := NewDataCenter("dc1")
topo.LinkChildNode(dc)
return topo, dc
}
func TestMaintenanceModeReportsNoFreeSlots(t *testing.T) {
rack := NewRack("rack1")
dn := addMaintenanceTestNode(rack, "server1", 10)
option := &VolumeGrowOption{DiskType: types.HardDriveType}
if got := dn.AvailableSpaceFor(option); got != 10 {
t.Fatalf("AvailableSpaceFor = %d, want 10", got)
}
if !dn.SetMaintenanceMode(true) {
t.Fatal("first switch should report a change")
}
if dn.SetMaintenanceMode(true) {
t.Fatal("repeating the same value should not report a change")
}
if !dn.InMaintenanceMode() {
t.Fatal("InMaintenanceMode = false after switching on")
}
if got := dn.AvailableSpaceFor(option); got != 0 {
t.Errorf("AvailableSpaceFor in maintenance = %d, want 0: no new volumes go to a server in maintenance", got)
}
if got := dn.AvailableSpaceForReservation(option); got != 0 {
t.Errorf("AvailableSpaceForReservation in maintenance = %d, want 0", got)
}
if _, ok := dn.TryReserveCapacity(types.HardDriveType, 1); ok {
t.Error("reserved capacity on a server in maintenance")
}
if got := rack.AvailableSpaceFor(option); got != 10 {
t.Errorf("rack AvailableSpaceFor = %d, want 10: the node's capacity still rolls up to its parents", got)
}
if !dn.SetMaintenanceMode(false) {
t.Fatal("switching off should report a change")
}
if got := dn.AvailableSpaceFor(option); got != 10 {
t.Errorf("AvailableSpaceFor after maintenance = %d, want 10", got)
}
}
// A server in maintenance mode is the one being evacuated, so volume growth in
// its rack must land every replica on its siblings even when it has the most
// free slots by far.
func TestVolumeGrowthSkipsMaintenanceNodeInSameRack(t *testing.T) {
topo, dc := newMaintenanceTestTopology()
rack := NewRack("rack1")
dc.LinkChildNode(rack)
busy := addMaintenanceTestNode(rack, "busy", 1000)
busy.SetMaintenanceMode(true)
addMaintenanceTestNode(rack, "small1", 2)
addMaintenanceTestNode(rack, "small2", 2)
vg := NewDefaultVolumeGrowth()
rp, _ := super_block.NewReplicaPlacementFromString("001")
option := &VolumeGrowOption{ReplicaPlacement: rp, DiskType: types.HardDriveType}
for i := 0; i < 50; i++ {
servers, _, err := vg.findEmptySlotsForOneVolume(topo, option, false)
if err != nil {
t.Fatalf("iteration %d: %v", i, err)
}
if len(servers) != 2 {
t.Fatalf("iteration %d: got %d servers, want 2", i, len(servers))
}
for _, s := range servers {
if s.Id() == busy.Id() {
t.Fatalf("iteration %d: picked the server in maintenance mode", i)
}
}
}
// Pinning the request to the maintenance node must fail rather than
// create a volume there.
rp0, _ := super_block.NewReplicaPlacementFromString("000")
pinned := &VolumeGrowOption{ReplicaPlacement: rp0, DiskType: types.HardDriveType, DataNode: "busy"}
if _, _, err := vg.findEmptySlotsForOneVolume(topo, pinned, false); err == nil {
t.Fatal("a volume pinned to a server in maintenance mode must not be created")
}
}
// The replica on another rack is chosen by walking a random offset through the
// rack's rolled-up free slots, which still count a node in maintenance. The walk
// has to land on an eligible sibling every time, not fall off the end.
func TestVolumeGrowthSkipsMaintenanceNodeInOtherRack(t *testing.T) {
topo, dc := newMaintenanceTestTopology()
rack1 := NewRack("rack1")
dc.LinkChildNode(rack1)
addMaintenanceTestNode(rack1, "r1n1", 100)
rack2 := NewRack("rack2")
dc.LinkChildNode(rack2)
busy := addMaintenanceTestNode(rack2, "r2busy", 1000)
busy.SetMaintenanceMode(true)
ok := addMaintenanceTestNode(rack2, "r2ok", 1)
vg := NewDefaultVolumeGrowth()
rp, _ := super_block.NewReplicaPlacementFromString("010")
option := &VolumeGrowOption{ReplicaPlacement: rp, DiskType: types.HardDriveType}
for i := 0; i < 50; i++ {
servers, _, err := vg.findEmptySlotsForOneVolume(topo, option, false)
if err != nil {
t.Fatalf("iteration %d: %v", i, err)
}
if len(servers) != 2 {
t.Fatalf("iteration %d: got %d servers, want 2", i, len(servers))
}
onSibling := 0
for _, s := range servers {
if s.Id() == busy.Id() {
t.Fatalf("iteration %d: picked the server in maintenance mode", i)
}
if s.Id() == ok.Id() {
onSibling++
}
}
if onSibling != 1 {
t.Fatalf("iteration %d: the other-rack replica must land on the eligible sibling, got %v", i, servers)
}
}
}
// A replica on a server in maintenance mode takes no writes, so its volume
// leaves the writable list while the mode is on and returns once it is off.
func TestMaintenanceModeTogglesVolumeWritability(t *testing.T) {
topo, dc := newMaintenanceTestTopology()
rack := NewRack("rack1")
dc.LinkChildNode(rack)
dn1 := addMaintenanceTestNode(rack, "dn1", 10)
dn2 := addMaintenanceTestNode(rack, "dn2", 10)
rp, _ := super_block.NewReplicaPlacementFromString("001")
shared := storage.VolumeInfo{Id: 1, Size: 100, ReplicaPlacement: rp, Ttl: needle.EMPTY_TTL, Version: needle.GetCurrentVersion()}
dn1.AddOrUpdateVolume(shared)
dn2.AddOrUpdateVolume(shared)
topo.RegisterVolumeLayout(shared, dn1)
topo.RegisterVolumeLayout(shared, dn2)
rp0, _ := super_block.NewReplicaPlacementFromString("000")
alone := storage.VolumeInfo{Id: 2, Size: 100, ReplicaPlacement: rp0, Ttl: needle.EMPTY_TTL, Version: needle.GetCurrentVersion()}
dn2.AddOrUpdateVolume(alone)
topo.RegisterVolumeLayout(alone, dn2)
vlShared := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
vlAlone := topo.GetVolumeLayout("", rp0, needle.EMPTY_TTL, types.HardDriveType)
expectWritables := func(step string, vl *VolumeLayout, want ...needle.VolumeId) {
t.Helper()
got := vl.CloneWritableVolumes()
slices.Sort(got)
if !slices.Equal(got, want) {
t.Errorf("%s: writables = %v, want %v", step, got, want)
}
}
expectWritables("initial shared", vlShared, 1)
expectWritables("initial alone", vlAlone, 2)
if !topo.SetDataNodeMaintenanceMode(dn1, true) {
t.Fatal("switching dn1 on should report a change")
}
expectWritables("dn1 in maintenance: a volume with a replica there takes no writes", vlShared)
expectWritables("dn1 in maintenance: volumes elsewhere are unaffected", vlAlone, 2)
if locations := vlShared.Lookup(1); len(locations) != 2 {
t.Errorf("reads must still see every replica, got %v", locations)
}
if topo.SetDataNodeMaintenanceMode(dn1, true) {
t.Error("repeating the same state should not report a change")
}
if !topo.SetDataNodeMaintenanceMode(dn1, false) {
t.Fatal("switching dn1 off should report a change")
}
expectWritables("dn1 back: the volume is writable again", vlShared, 1)
topo.SetDataNodeMaintenanceMode(dn2, true)
expectWritables("dn2 in maintenance: shared", vlShared)
expectWritables("dn2 in maintenance: alone", vlAlone)
}
// A vacuum commit or a mark-writable that lands after the server entered
// maintenance mode must not put the volume back on the writable list. Both
// used to check only the replica count, and since heartbeats carry only changed
// volumes, nothing would have taken it off again.
func TestMaintenanceModeHoldsThroughVacuumAndMarkWritable(t *testing.T) {
topo, dc := newMaintenanceTestTopology()
rack := NewRack("rack1")
dc.LinkChildNode(rack)
dn1 := addMaintenanceTestNode(rack, "dn1", 10)
dn2 := addMaintenanceTestNode(rack, "dn2", 10)
rp, _ := super_block.NewReplicaPlacementFromString("001")
vi := storage.VolumeInfo{Id: 1, Size: 100, ReplicaPlacement: rp, Ttl: needle.EMPTY_TTL, Version: needle.GetCurrentVersion()}
dn1.AddOrUpdateVolume(vi)
dn2.AddOrUpdateVolume(vi)
topo.RegisterVolumeLayout(vi, dn1)
topo.RegisterVolumeLayout(vi, dn2)
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
expectWritableCount := func(step string, want int) {
t.Helper()
if got, _ := vl.GetWritableVolumeCount(); got != want {
t.Errorf("%s: writable count = %d, want %d", step, got, want)
}
}
// vacuum: taken off the writable list before compaction, dn1 enters
// maintenance meanwhile, then every replica commits
vl.DrainAndRemoveFromWritable(1)
topo.SetDataNodeMaintenanceMode(dn1, true)
for _, dn := range []*DataNode{dn1, dn2} {
if vl.SetVolumeAvailable(dn, 1, false, false) {
t.Errorf("vacuum commit on %s made the volume writable with a replica in maintenance", dn.Id())
}
}
expectWritableCount("after vacuum commit", 0)
topo.SetDataNodeMaintenanceMode(dn1, false)
expectWritableCount("after maintenance", 1)
// mark-writable from a vacuum worker, after dn1 entered maintenance
vl.SetVolumeReadOnly(dn2, 1)
topo.SetDataNodeMaintenanceMode(dn1, true)
if vl.SetVolumeWritable(dn2, 1) {
t.Error("mark-writable made the volume writable with a replica in maintenance")
}
expectWritableCount("after mark-writable", 0)
topo.SetDataNodeMaintenanceMode(dn1, false)
expectWritableCount("after maintenance", 1)
}
// A volume server notifies the master the moment it flips a volume between
// read-only and writable, ahead of the heartbeat that repeats the flag. The
// master must act on the notification, not on its heartbeat copy: mark-writable
// takes effect at once, and a maintenance toggle landing between a mark-readonly
// and its heartbeat must not put the volume back.
func TestMarkReadOnlyAndWritableAheadOfHeartbeat(t *testing.T) {
topo, dc := newMaintenanceTestTopology()
rack := NewRack("rack1")
dc.LinkChildNode(rack)
dn1 := addMaintenanceTestNode(rack, "dn1", 10)
dn2 := addMaintenanceTestNode(rack, "dn2", 10)
rp, _ := super_block.NewReplicaPlacementFromString("001")
vi := storage.VolumeInfo{Id: 1, Size: 100, ReplicaPlacement: rp, Ttl: needle.EMPTY_TTL, Version: needle.GetCurrentVersion()}
dn1.AddOrUpdateVolume(vi)
dn2.AddOrUpdateVolume(vi)
topo.RegisterVolumeLayout(vi, dn1)
topo.RegisterVolumeLayout(vi, dn2)
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
expectWritableCount := func(step string, want int) {
t.Helper()
if got, _ := vl.GetWritableVolumeCount(); got != want {
t.Errorf("%s: writable count = %d, want %d", step, got, want)
}
}
// the heartbeat has reported dn2's replica read-only
readOnly := vi
readOnly.ReadOnly = true
dn2.AddOrUpdateVolume(readOnly)
vl.EnsureCorrectWritables(&readOnly)
expectWritableCount("heartbeat says read-only", 0)
// dn2 marks it writable and notifies, before the next heartbeat
if !vl.SetVolumeWritable(dn2, 1) {
t.Error("mark-writable from the replica's own server should take effect at once")
}
expectWritableCount("after mark-writable", 1)
if v, err := dn2.GetVolumesById(1); err != nil || v.ReadOnly {
t.Errorf("the node's record should say writable, got %+v, %v", v, err)
}
// dn2 marks it read-only and notifies; dn1 leaves maintenance before the
// heartbeat repeats the flag
vl.SetVolumeReadOnly(dn2, 1)
expectWritableCount("after mark-readonly", 0)
topo.SetDataNodeMaintenanceMode(dn1, true)
topo.SetDataNodeMaintenanceMode(dn1, false)
expectWritableCount("re-evaluated before the heartbeat", 0)
}
// SetDataNodeMaintenanceMode walks a snapshot of the node's volumes, so a
// concurrent disconnect can have removed one from its layout by the time it is
// re-evaluated. That must be a no-op, not a crash or a resurrected entry.
func TestMaintenanceModeAfterVolumeLeftLayout(t *testing.T) {
topo, dc := newMaintenanceTestTopology()
rack := NewRack("rack1")
dc.LinkChildNode(rack)
dn := addMaintenanceTestNode(rack, "dn1", 10)
rp, _ := super_block.NewReplicaPlacementFromString("000")
vi := storage.VolumeInfo{Id: 1, Size: 100, ReplicaPlacement: rp, Ttl: needle.EMPTY_TTL, Version: needle.GetCurrentVersion()}
dn.AddOrUpdateVolume(vi)
topo.RegisterVolumeLayout(vi, dn)
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
vl.UnRegisterVolume(&vi, dn)
for _, on := range []bool{true, false} {
if !topo.SetDataNodeMaintenanceMode(dn, on) {
t.Fatalf("SetDataNodeMaintenanceMode(%v) should report a change", on)
}
if got, _ := vl.GetWritableVolumeCount(); got != 0 {
t.Errorf("maintenance %v: writable count = %d, want 0", on, got)
}
if locations := vl.Lookup(1); locations != nil {
t.Errorf("maintenance %v: re-evaluating a departed volume re-created its location list: %v", on, locations)
}
}
}

602
weed/topology/node.go Normal file
View File

@@ -0,0 +1,602 @@
package topology
import (
"errors"
"fmt"
"math/rand/v2"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
type NodeId string
// CapacityReservation represents a temporary reservation of capacity
type CapacityReservation struct {
reservationId string
diskType types.DiskType
count int64
createdAt time.Time
}
// CapacityReservations manages capacity reservations for a node
type CapacityReservations struct {
sync.RWMutex
reservations map[string]*CapacityReservation
reservedCounts map[types.DiskType]int64
}
func newCapacityReservations() *CapacityReservations {
return &CapacityReservations{
reservations: make(map[string]*CapacityReservation),
reservedCounts: make(map[types.DiskType]int64),
}
}
func (cr *CapacityReservations) removeReservation(reservationId string) bool {
cr.Lock()
defer cr.Unlock()
if reservation, exists := cr.reservations[reservationId]; exists {
delete(cr.reservations, reservationId)
cr.decrementCount(reservation.diskType, reservation.count)
return true
}
return false
}
func (cr *CapacityReservations) getReservedCount(diskType types.DiskType) int64 {
cr.RLock()
defer cr.RUnlock()
return cr.reservedCounts[diskType]
}
// decrementCount is a helper to decrement reserved count and clean up zero entries
func (cr *CapacityReservations) decrementCount(diskType types.DiskType, count int64) {
cr.reservedCounts[diskType] -= count
// Clean up zero counts to prevent map growth
if cr.reservedCounts[diskType] <= 0 {
delete(cr.reservedCounts, diskType)
}
}
// doAddReservation is a helper to add a reservation, assuming the lock is already held
func (cr *CapacityReservations) doAddReservation(diskType types.DiskType, count int64) string {
now := time.Now()
reservationId := fmt.Sprintf("%s-%d-%d-%d", diskType, count, now.UnixNano(), rand.Int64())
cr.reservations[reservationId] = &CapacityReservation{
reservationId: reservationId,
diskType: diskType,
count: count,
createdAt: now,
}
cr.reservedCounts[diskType] += count
return reservationId
}
// tryReserveAtomic atomically checks available space and reserves if possible
func (cr *CapacityReservations) tryReserveAtomic(diskType types.DiskType, count int64, availableSpaceFunc func() int64) (reservationId string, success bool) {
cr.Lock()
defer cr.Unlock()
// Check available space under lock
currentReserved := cr.reservedCounts[diskType]
availableSpace := availableSpaceFunc() - currentReserved
if availableSpace >= count {
// Create and add reservation atomically
return cr.doAddReservation(diskType, count), true
}
return "", false
}
func (cr *CapacityReservations) cleanExpiredReservations(expirationDuration time.Duration) {
cr.Lock()
defer cr.Unlock()
now := time.Now()
for id, reservation := range cr.reservations {
if now.Sub(reservation.createdAt) > expirationDuration {
delete(cr.reservations, id)
cr.decrementCount(reservation.diskType, reservation.count)
glog.V(1).Infof("Cleaned up expired capacity reservation: %s", id)
}
}
}
type Node interface {
Id() NodeId
String() string
AvailableSpaceFor(option *VolumeGrowOption) int64
ReserveOneVolume(r int64, option *VolumeGrowOption) (*DataNode, error)
ReserveOneVolumeForReservation(r int64, option *VolumeGrowOption) (*DataNode, error)
UpAdjustDiskUsageDelta(diskType types.DiskType, diskUsage *DiskUsageCounts)
UpAdjustMaxVolumeId(vid needle.VolumeId)
GetDiskUsages() *DiskUsages
// Capacity reservation methods for avoiding race conditions
TryReserveCapacity(diskType types.DiskType, count int64) (reservationId string, success bool)
ReleaseReservedCapacity(reservationId string)
AvailableSpaceForReservation(option *VolumeGrowOption) int64
GetMaxVolumeId() needle.VolumeId
SetParent(Node)
LinkChildNode(node Node)
UnlinkChildNode(nodeId NodeId)
CollectDeadNodeAndFullVolumes(freshThreshHold int64, volumeSizeLimit uint64, growThreshold float64)
IsDataNode() bool
IsRack() bool
IsDataCenter() bool
IsLocked() bool
Children() []Node
Parent() Node
GetValue() interface{} //get reference to the topology,dc,rack,datanode
}
type NodeImpl struct {
diskUsages *DiskUsages
id NodeId
parent Node
sync.RWMutex // lock children
children map[NodeId]Node
// maxVolumeId uses atomic ops so UpAdjustMaxVolumeId (called from the
// volume server heartbeat path) and GetMaxVolumeId (called from the
// master's assign / warmup checks) can run concurrently without a
// data race on NodeImpl.
maxVolumeId atomic.Uint32
//for rack, data center, topology
nodeType string
value interface{}
// capacity reservations to prevent race conditions during volume creation
capacityReservations *CapacityReservations
}
func (n *NodeImpl) GetDiskUsages() *DiskUsages {
return n.diskUsages
}
// nodeHost returns the host a node runs on, or "" for non-data-node tiers (data
// centers, racks).
func nodeHost(node Node) string {
if dn, ok := node.(*DataNode); ok {
return dn.Ip
}
return ""
}
// preferDistinctHosts reorders candidates so the first node of each not-yet-used
// host comes first (preserving weighted order), then the same-host leftovers, so a
// prefix covers the most distinct machines. usedHost seeds the set. No-op when
// hosts are empty (non-data-node tiers).
func preferDistinctHosts(usedHost string, candidates []Node) []Node {
used := map[string]bool{}
if usedHost != "" {
used[usedHost] = true
}
distinct := make([]Node, 0, len(candidates))
dup := make([]Node, 0, len(candidates))
for _, node := range candidates {
h := nodeHost(node)
if h != "" && !used[h] {
used[h] = true
distinct = append(distinct, node)
} else {
dup = append(dup, node)
}
}
return append(distinct, dup...)
}
// the first node must satisfy filterFirstNodeFn(), the rest nodes must have one free slot
func (n *NodeImpl) PickNodesByWeight(numberOfNodes int, option *VolumeGrowOption, filterFirstNodeFn func(dn Node) error) (firstNode Node, restNodes []Node, err error) {
var totalWeights int64
var errs []string
n.RLock()
candidates := make([]Node, 0, len(n.children))
candidatesWeights := make([]int64, 0, len(n.children))
//pick nodes which has enough free volumes as candidates, and use free volumes number as node weight.
for _, node := range n.children {
if node.AvailableSpaceFor(option) <= 0 {
continue
}
totalWeights += node.AvailableSpaceFor(option)
candidates = append(candidates, node)
candidatesWeights = append(candidatesWeights, node.AvailableSpaceFor(option))
}
n.RUnlock()
if len(candidates) < numberOfNodes {
glog.V(0).Infoln(n.Id(), "failed to pick", numberOfNodes, "from ", len(candidates), "node candidates")
return nil, nil, errors.New("Not enough data nodes found!")
}
//pick nodes randomly by weights, the node picked earlier has higher final weights
sortedCandidates := make([]Node, 0, len(candidates))
for i := 0; i < len(candidates); i++ {
// Break if no more weights available to prevent panic in rand.Int64N
if totalWeights <= 0 {
break
}
weightsInterval := rand.Int64N(totalWeights)
lastWeights := int64(0)
for k, weights := range candidatesWeights {
if (weightsInterval >= lastWeights) && (weightsInterval < lastWeights+weights) {
sortedCandidates = append(sortedCandidates, candidates[k])
candidatesWeights[k] = 0
totalWeights -= weights
break
}
lastWeights += weights
}
}
restNodes = make([]Node, 0, numberOfNodes-1)
ret := false
n.RLock()
for k, node := range sortedCandidates {
if err := filterFirstNodeFn(node); err == nil {
firstNode = node
// Fill the rest preferring not-yet-used hosts, so replicas spread across
// machines; falls back to same-host when too few. No-op for dc/rack tiers
// (empty host), which keep the weighted order.
pool := make([]Node, 0, len(sortedCandidates)-1)
pool = append(pool, sortedCandidates[:k]...)
pool = append(pool, sortedCandidates[k+1:]...)
pool = preferDistinctHosts(nodeHost(firstNode), pool)
if len(pool) > numberOfNodes-1 {
pool = pool[:numberOfNodes-1]
}
restNodes = pool
ret = true
break
} else {
errs = append(errs, string(node.Id())+":"+err.Error())
}
}
n.RUnlock()
if !ret {
return nil, nil, errors.New("No matching data node found! \n" + strings.Join(errs, "\n"))
}
return
}
func (n *NodeImpl) IsDataNode() bool {
return n.nodeType == "DataNode"
}
func (n *NodeImpl) IsRack() bool {
return n.nodeType == "Rack"
}
func (n *NodeImpl) IsDataCenter() bool {
return n.nodeType == "DataCenter"
}
func (n *NodeImpl) IsLocked() (isTryLock bool) {
if isTryLock = n.TryRLock(); isTryLock {
n.RUnlock()
}
return !isTryLock
}
func (n *NodeImpl) String() string {
if n.parent != nil {
return n.parent.String() + ":" + string(n.id)
}
return string(n.id)
}
func (n *NodeImpl) Id() NodeId {
return n.id
}
func (n *NodeImpl) getOrCreateDisk(diskType types.DiskType) *DiskUsageCounts {
return n.diskUsages.getOrCreateDisk(diskType)
}
// inMaintenanceMode is true for a data node whose volume server is in
// maintenance mode. Racks and data centers never are: their rolled-up counters
// still include such a node, so their free-slot totals may exceed what their
// children will actually hand out; see reserveOneVolumeInternal.
func (n *NodeImpl) inMaintenanceMode() bool {
dn, ok := n.value.(*DataNode)
return ok && dn.InMaintenanceMode()
}
// AvailableSpaceFor is the free volume slots on this node for the option's disk
// type. A data node in maintenance mode reports none: it is being drained, so
// it is neither a volume-growth candidate nor reservable.
func (n *NodeImpl) AvailableSpaceFor(option *VolumeGrowOption) int64 {
if n.inMaintenanceMode() {
return 0
}
t := n.getOrCreateDisk(option.DiskType)
freeVolumeSlotCount := atomic.LoadInt64(&t.maxVolumeCount) + atomic.LoadInt64(&t.remoteVolumeCount) - atomic.LoadInt64(&t.volumeCount)
freeVolumeSlotCount -= erasure_coding.VolumeSlots(atomic.LoadInt64(&t.ecShardCount))
return freeVolumeSlotCount
}
// CapacityFor is the total registered volume slots for the option's disk type;
// zero means no volume server has reported capacity for it yet.
func (n *NodeImpl) CapacityFor(option *VolumeGrowOption) int64 {
t := n.getOrCreateDisk(option.DiskType)
return atomic.LoadInt64(&t.maxVolumeCount) + atomic.LoadInt64(&t.remoteVolumeCount)
}
// CapacityForAnyDisk is the total registered volume slots across every disk
// type. CapacityFor answers zero both while a cluster is still starting and
// when it never serves the option's medium; this tells the two apart.
func (n *NodeImpl) CapacityForAnyDisk() (total int64) {
n.diskUsages.RLock()
defer n.diskUsages.RUnlock()
for _, t := range n.diskUsages.usages {
total += atomic.LoadInt64(&t.maxVolumeCount) + atomic.LoadInt64(&t.remoteVolumeCount)
}
return
}
// AvailableSpaceForReservation returns available space considering existing reservations
func (n *NodeImpl) AvailableSpaceForReservation(option *VolumeGrowOption) int64 {
baseAvailable := n.AvailableSpaceFor(option)
reservedCount := n.capacityReservations.getReservedCount(option.DiskType)
return baseAvailable - reservedCount
}
// TryReserveCapacity attempts to atomically reserve capacity for volume creation
func (n *NodeImpl) TryReserveCapacity(diskType types.DiskType, count int64) (reservationId string, success bool) {
const reservationTimeout = 5 * time.Minute // TODO: make this configurable
// Clean up any expired reservations first
n.capacityReservations.cleanExpiredReservations(reservationTimeout)
// Atomically check and reserve space
option := &VolumeGrowOption{DiskType: diskType}
reservationId, success = n.capacityReservations.tryReserveAtomic(diskType, count, func() int64 {
return n.AvailableSpaceFor(option)
})
if success {
glog.V(1).Infof("Reserved %d capacity for diskType %s on node %s: %s", count, diskType, n.Id(), reservationId)
}
return reservationId, success
}
// ReleaseReservedCapacity releases a previously reserved capacity
func (n *NodeImpl) ReleaseReservedCapacity(reservationId string) {
if n.capacityReservations.removeReservation(reservationId) {
glog.V(1).Infof("Released capacity reservation on node %s: %s", n.Id(), reservationId)
} else {
glog.V(1).Infof("Attempted to release non-existent reservation on node %s: %s", n.Id(), reservationId)
}
}
func (n *NodeImpl) SetParent(node Node) {
n.parent = node
}
func (n *NodeImpl) Children() (ret []Node) {
n.RLock()
defer n.RUnlock()
for _, c := range n.children {
ret = append(ret, c)
}
return ret
}
func (n *NodeImpl) Parent() Node {
return n.parent
}
func (n *NodeImpl) GetValue() interface{} {
return n.value
}
func (n *NodeImpl) ReserveOneVolume(r int64, option *VolumeGrowOption) (assignedNode *DataNode, err error) {
return n.reserveOneVolumeInternal(r, option, false)
}
// ReserveOneVolumeForReservation selects a node using reservation-aware capacity checks
func (n *NodeImpl) ReserveOneVolumeForReservation(r int64, option *VolumeGrowOption) (assignedNode *DataNode, err error) {
return n.reserveOneVolumeInternal(r, option, true)
}
func (n *NodeImpl) reserveOneVolumeInternal(r int64, option *VolumeGrowOption, useReservations bool) (assignedNode *DataNode, err error) {
n.RLock()
defer n.RUnlock()
freeSpaceOf := func(node Node) int64 {
if useReservations {
return node.AvailableSpaceForReservation(option)
}
return node.AvailableSpaceFor(option)
}
// The caller draws r from this node's rolled-up free slots, which still
// count children the walk below skips: a data node in maintenance mode
// reports no space, and an over-committed one reports less than zero. Fold
// r into the space that is actually on offer so it cannot walk off the end
// and fail with slots still free.
var eligible int64
for _, node := range n.children {
if freeSpace := freeSpaceOf(node); freeSpace > 0 {
eligible += freeSpace
}
}
if eligible <= 0 {
return nil, errors.New("No free volume slot found!")
}
r %= eligible
for _, node := range n.children {
freeSpace := freeSpaceOf(node)
// fmt.Println("r =", r, ", node =", node, ", freeSpace =", freeSpace)
if freeSpace <= 0 {
continue
}
if r >= freeSpace {
r -= freeSpace
} else {
var hasSpace bool
if useReservations {
hasSpace = node.IsDataNode() && node.AvailableSpaceForReservation(option) > 0
} else {
hasSpace = node.IsDataNode() && node.AvailableSpaceFor(option) > 0
}
if hasSpace {
// fmt.Println("vid =", vid, " assigned to node =", node, ", freeSpace =", node.FreeSpace())
dn := node.(*DataNode)
if dn.IsTerminating {
continue
}
return dn, nil
}
if useReservations {
assignedNode, err = node.ReserveOneVolumeForReservation(r, option)
} else {
assignedNode, err = node.ReserveOneVolume(r, option)
}
if err == nil {
return
}
}
}
return nil, errors.New("No free volume slot found!")
}
func (n *NodeImpl) UpAdjustDiskUsageDelta(diskType types.DiskType, diskUsage *DiskUsageCounts) { //can be negative
existingDisk := n.getOrCreateDisk(diskType)
existingDisk.addDiskUsageCounts(diskUsage)
if n.parent != nil {
n.parent.UpAdjustDiskUsageDelta(diskType, diskUsage)
}
}
func (n *NodeImpl) UpAdjustMaxVolumeId(vid needle.VolumeId) {
target := uint32(vid)
for {
current := n.maxVolumeId.Load()
if current >= target {
return
}
if n.maxVolumeId.CompareAndSwap(current, target) {
break
}
}
if n.parent != nil {
n.parent.UpAdjustMaxVolumeId(vid)
}
}
func (n *NodeImpl) GetMaxVolumeId() needle.VolumeId {
return needle.VolumeId(n.maxVolumeId.Load())
}
func (n *NodeImpl) LinkChildNode(node Node) {
n.Lock()
defer n.Unlock()
n.doLinkChildNode(node)
}
func (n *NodeImpl) doLinkChildNode(node Node) {
if n.children[node.Id()] == nil {
n.children[node.Id()] = node
for dt, du := range node.GetDiskUsages().usages {
n.UpAdjustDiskUsageDelta(dt, du)
}
n.UpAdjustMaxVolumeId(node.GetMaxVolumeId())
node.SetParent(n)
// Maintain the topology's address index so Ping admission and other
// callers can resolve a data node from its address in O(1).
if dn, ok := node.GetValue().(*DataNode); ok {
if topo := n.GetTopology(); topo != nil {
topo.registerDataNodeAddress(dn)
}
}
glog.V(0).Infoln(n, "adds child", node.Id())
}
}
func (n *NodeImpl) UnlinkChildNode(nodeId NodeId) {
n.Lock()
defer n.Unlock()
node := n.children[nodeId]
if node != nil {
// Drop the topology address index before clearing the parent pointer
// so GetTopology() can still walk up to the root.
if dn, ok := node.GetValue().(*DataNode); ok {
if topo := n.GetTopology(); topo != nil {
topo.unregisterDataNodeAddress(dn.ServerAddress(), dn)
}
}
node.SetParent(nil)
delete(n.children, node.Id())
for dt, du := range node.GetDiskUsages().negative().usages {
n.UpAdjustDiskUsageDelta(dt, du)
}
glog.V(0).Infoln(n, "removes", node.Id())
}
}
func (n *NodeImpl) CollectDeadNodeAndFullVolumes(freshThreshHoldUnixTime int64, volumeSizeLimit uint64, growThreshold float64) {
if n.IsRack() {
for _, c := range n.Children() {
dn := c.(*DataNode) //can not cast n to DataNode
for _, v := range dn.GetVolumes() {
topo := n.GetTopology()
diskType := types.ToDiskType(v.DiskType)
vl := topo.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
if v.Size >= volumeSizeLimit {
vl.accessLock.RLock()
vacuumTime, ok := vl.vacuumedVolumes[v.Id]
vl.accessLock.RUnlock()
// If a volume has been vacuumed in the past 20 seconds, we do not check whether it has reached full capacity.
// After 20s(grpc timeout), theoretically all the heartbeats of the volume server have reached the master,
// the volume size should be correct, not the size before the vacuum.
if !ok || time.Now().Add(-20*time.Second).After(vacuumTime) {
//fmt.Println("volume",v.Id,"size",v.Size,">",volumeSizeLimit)
topo.chanFullVolumes <- v
}
} else if !v.ReadOnly && float64(v.Size) > float64(volumeSizeLimit)*growThreshold {
// Crowding asks for more room to write into, which a
// read-only volume can never provide. Growth already
// discounts them by intersecting with the writable list, so
// marking one only costs the entry.
topo.chanCrowdedVolumes <- v
}
copyCount := v.ReplicaPlacement.GetCopyCount()
if copyCount > 1 {
if copyCount > len(topo.Lookup(v.Collection, v.Id)) {
stats.MasterReplicaPlacementMismatch.WithLabelValues(v.Collection, v.Id.String()).Set(1)
} else {
stats.MasterReplicaPlacementMismatch.WithLabelValues(v.Collection, v.Id.String()).Set(0)
}
}
}
}
} else {
for _, c := range n.Children() {
c.CollectDeadNodeAndFullVolumes(freshThreshHoldUnixTime, volumeSizeLimit, growThreshold)
}
}
}
func (n *NodeImpl) GetTopology() *Topology {
var p Node = n
for p.Parent() != nil {
p = p.Parent()
}
// A detached subtree (no Topology root in scope) must not panic; the
// callers above check the returned value for nil and skip the
// address-index maintenance in that case.
topo, _ := p.GetValue().(*Topology)
return topo
}

View File

@@ -0,0 +1,350 @@
package topology
import (
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
// TestRaceConditionStress simulates the original issue scenario:
// High concurrent writes causing capacity misjudgment
func TestRaceConditionStress(t *testing.T) {
// Create a cluster similar to the issue description:
// 3 volume servers, 200GB each, 5GB volume limit = 40 volumes max per server
const (
numServers = 3
volumeLimitMB = 5000 // 5GB in MB
storagePerServerGB = 200 // 200GB per server
maxVolumesPerServer = storagePerServerGB * 1024 / volumeLimitMB // 200*1024/5000 = 40
concurrentRequests = 50 // High concurrency like the issue
)
// Create test topology
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), uint64(volumeLimitMB)*1024*1024, 5, false)
dc := NewDataCenter("dc1")
topo.LinkChildNode(dc)
rack := NewRack("rack1")
dc.LinkChildNode(rack)
// Create 3 volume servers with realistic capacity
servers := make([]*DataNode, numServers)
for i := 0; i < numServers; i++ {
dn := NewDataNode(fmt.Sprintf("server%d", i+1))
rack.LinkChildNode(dn)
// Set up disk with capacity for 40 volumes
disk := NewDisk(types.HardDriveType.String())
disk.diskUsages.getOrCreateDisk(types.HardDriveType).maxVolumeCount = maxVolumesPerServer
dn.LinkChildNode(disk)
servers[i] = dn
}
vg := NewDefaultVolumeGrowth()
rp, _ := super_block.NewReplicaPlacementFromString("000") // Single replica like the issue
option := &VolumeGrowOption{
Collection: "test-bucket-large", // Same collection name as issue
ReplicaPlacement: rp,
DiskType: types.HardDriveType,
}
// Track results
var successfulAllocations int64
var failedAllocations int64
var totalVolumesCreated int64
var wg sync.WaitGroup
// Launch concurrent volume creation requests
startTime := time.Now()
for i := 0; i < concurrentRequests; i++ {
wg.Add(1)
go func(requestId int) {
defer wg.Done()
// This is the critical test: multiple threads trying to allocate simultaneously
servers, reservation, err := vg.findEmptySlotsForOneVolume(topo, option, true)
if err != nil {
atomic.AddInt64(&failedAllocations, 1)
t.Logf("Request %d failed: %v", requestId, err)
return
}
// Simulate volume creation delay (like in real scenario)
time.Sleep(time.Millisecond * 50)
// Simulate successful volume creation
for _, server := range servers {
disk := server.children[NodeId(types.HardDriveType.String())].(*Disk)
deltaDiskUsage := &DiskUsageCounts{
volumeCount: 1,
}
disk.UpAdjustDiskUsageDelta(types.HardDriveType, deltaDiskUsage)
atomic.AddInt64(&totalVolumesCreated, 1)
}
// Release reservations (simulates successful registration)
reservation.releaseAllReservations()
atomic.AddInt64(&successfulAllocations, 1)
}(i)
}
wg.Wait()
duration := time.Since(startTime)
// Verify results
t.Logf("Test completed in %v", duration)
t.Logf("Successful allocations: %d", successfulAllocations)
t.Logf("Failed allocations: %d", failedAllocations)
t.Logf("Total volumes created: %d", totalVolumesCreated)
// Check capacity limits are respected
totalCapacityUsed := int64(0)
for i, server := range servers {
disk := server.children[NodeId(types.HardDriveType.String())].(*Disk)
volumeCount := disk.diskUsages.getOrCreateDisk(types.HardDriveType).volumeCount
totalCapacityUsed += volumeCount
t.Logf("Server %d: %d volumes (max: %d)", i+1, volumeCount, maxVolumesPerServer)
// Critical test: No server should exceed its capacity
if volumeCount > maxVolumesPerServer {
t.Errorf("RACE CONDITION DETECTED: Server %d exceeded capacity: %d > %d",
i+1, volumeCount, maxVolumesPerServer)
}
}
// Verify totals make sense
if totalVolumesCreated != totalCapacityUsed {
t.Errorf("Volume count mismatch: created=%d, actual=%d", totalVolumesCreated, totalCapacityUsed)
}
// The total should never exceed the cluster capacity (120 volumes for 3 servers × 40 each)
maxClusterCapacity := int64(numServers * maxVolumesPerServer)
if totalCapacityUsed > maxClusterCapacity {
t.Errorf("RACE CONDITION DETECTED: Cluster capacity exceeded: %d > %d",
totalCapacityUsed, maxClusterCapacity)
}
// With reservations, we should have controlled allocation
// Total requests = successful + failed should equal concurrentRequests
if successfulAllocations+failedAllocations != concurrentRequests {
t.Errorf("Request count mismatch: success=%d + failed=%d != total=%d",
successfulAllocations, failedAllocations, concurrentRequests)
}
t.Logf("Race condition test passed: Capacity limits respected with %d concurrent requests",
concurrentRequests)
}
// TestCapacityJudgmentAccuracy verifies that the capacity calculation is accurate
// under various load conditions
func TestCapacityJudgmentAccuracy(t *testing.T) {
// Create a single server with known capacity
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 5*1024*1024*1024, 5, false)
dc := NewDataCenter("dc1")
topo.LinkChildNode(dc)
rack := NewRack("rack1")
dc.LinkChildNode(rack)
dn := NewDataNode("server1")
rack.LinkChildNode(dn)
// Server with capacity for exactly 10 volumes
disk := NewDisk(types.HardDriveType.String())
diskUsage := disk.diskUsages.getOrCreateDisk(types.HardDriveType)
diskUsage.maxVolumeCount = 10
dn.LinkChildNode(disk)
// Also set max volume count on the DataNode level (gets propagated up)
dn.diskUsages.getOrCreateDisk(types.HardDriveType).maxVolumeCount = 10
vg := NewDefaultVolumeGrowth()
rp, _ := super_block.NewReplicaPlacementFromString("000")
option := &VolumeGrowOption{
Collection: "test",
ReplicaPlacement: rp,
DiskType: types.HardDriveType,
}
// Test accurate capacity reporting at each step
for i := 0; i < 10; i++ {
// Check available space before reservation
availableBefore := dn.AvailableSpaceFor(option)
availableForReservation := dn.AvailableSpaceForReservation(option)
expectedAvailable := int64(10 - i)
if availableBefore != expectedAvailable {
t.Errorf("Step %d: Expected %d available, got %d", i, expectedAvailable, availableBefore)
}
if availableForReservation != expectedAvailable {
t.Errorf("Step %d: Expected %d available for reservation, got %d", i, expectedAvailable, availableForReservation)
}
// Try to reserve and allocate
_, reservation, err := vg.findEmptySlotsForOneVolume(topo, option, true)
if err != nil {
t.Fatalf("Step %d: Unexpected reservation failure: %v", i, err)
}
// Check that available space for reservation decreased
availableAfterReservation := dn.AvailableSpaceForReservation(option)
if availableAfterReservation != expectedAvailable-1 {
t.Errorf("Step %d: Expected %d available after reservation, got %d",
i, expectedAvailable-1, availableAfterReservation)
}
// Simulate successful volume creation by properly updating disk usage hierarchy
disk := dn.children[NodeId(types.HardDriveType.String())].(*Disk)
// Create a volume usage delta to simulate volume creation
deltaDiskUsage := &DiskUsageCounts{
volumeCount: 1,
}
// Properly propagate the usage up the hierarchy
disk.UpAdjustDiskUsageDelta(types.HardDriveType, deltaDiskUsage)
// Debug: Check the volume count after update
diskUsageOnNode := dn.diskUsages.getOrCreateDisk(types.HardDriveType)
currentVolumeCount := atomic.LoadInt64(&diskUsageOnNode.volumeCount)
t.Logf("Step %d: Volume count after update: %d", i, currentVolumeCount)
// Release reservation
reservation.releaseAllReservations()
// Verify final state
availableAfter := dn.AvailableSpaceFor(option)
expectedAfter := int64(10 - i - 1)
if availableAfter != expectedAfter {
t.Errorf("Step %d: Expected %d available after creation, got %d",
i, expectedAfter, availableAfter)
// More debugging
diskUsageOnNode := dn.diskUsages.getOrCreateDisk(types.HardDriveType)
maxVolumes := atomic.LoadInt64(&diskUsageOnNode.maxVolumeCount)
remoteVolumes := atomic.LoadInt64(&diskUsageOnNode.remoteVolumeCount)
actualVolumeCount := atomic.LoadInt64(&diskUsageOnNode.volumeCount)
t.Logf("Debug Step %d: max=%d, volume=%d, remote=%d", i, maxVolumes, actualVolumeCount, remoteVolumes)
}
}
// At this point, no more reservations should succeed
_, _, err := vg.findEmptySlotsForOneVolume(topo, option, true)
if err == nil {
t.Error("Expected reservation to fail when at capacity")
}
t.Logf("Capacity judgment accuracy test passed")
}
// TestReservationSystemPerformance measures the performance impact of reservations
func TestReservationSystemPerformance(t *testing.T) {
// Create topology
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := NewDataCenter("dc1")
topo.LinkChildNode(dc)
rack := NewRack("rack1")
dc.LinkChildNode(rack)
dn := NewDataNode("server1")
rack.LinkChildNode(dn)
disk := NewDisk(types.HardDriveType.String())
disk.diskUsages.getOrCreateDisk(types.HardDriveType).maxVolumeCount = 1000
dn.LinkChildNode(disk)
vg := NewDefaultVolumeGrowth()
rp, _ := super_block.NewReplicaPlacementFromString("000")
option := &VolumeGrowOption{
Collection: "test",
ReplicaPlacement: rp,
DiskType: types.HardDriveType,
}
// Benchmark reservation operations
const iterations = 1000
startTime := time.Now()
for i := 0; i < iterations; i++ {
_, reservation, err := vg.findEmptySlotsForOneVolume(topo, option, true)
if err != nil {
t.Fatalf("Iteration %d failed: %v", i, err)
}
reservation.releaseAllReservations()
// Simulate volume creation
diskUsage := dn.diskUsages.getOrCreateDisk(types.HardDriveType)
atomic.AddInt64(&diskUsage.volumeCount, 1)
}
duration := time.Since(startTime)
avgDuration := duration / iterations
t.Logf("Performance: %d reservations in %v (avg: %v per reservation)",
iterations, duration, avgDuration)
// Performance should be reasonable (less than 1ms per reservation on average)
if avgDuration > time.Millisecond {
t.Errorf("Reservation system performance concern: %v per reservation", avgDuration)
} else {
t.Logf("Performance test passed: %v per reservation", avgDuration)
}
}
func TestDisk_GetEcShards_Race(t *testing.T) {
d := NewDisk("hdd")
// Pre-populate with one shard
initialShard := &erasure_coding.EcVolumeInfo{
VolumeId: needle.VolumeId(1),
ShardsInfo: erasure_coding.NewShardsInfo(),
}
initialShard.ShardsInfo.Set(erasure_coding.ShardInfo{Id: 0, Size: 100})
d.AddOrUpdateEcShard(initialShard)
var wg sync.WaitGroup
wg.Add(10)
// Goroutine 1-5: Continuously read shards
for j := 0; j < 5; j++ {
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
d.GetEcShards()
}
}()
}
// Goroutine 6-10: Continuously update shards
for j := 0; j < 5; j++ {
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
shard := &erasure_coding.EcVolumeInfo{
VolumeId: needle.VolumeId(i % 100),
ShardsInfo: erasure_coding.NewShardsInfo(),
}
shard.ShardsInfo.Set(erasure_coding.ShardInfo{Id: erasure_coding.ShardId(i % 14), Size: 100})
d.AddOrUpdateEcShard(shard)
}
}()
}
wg.Wait()
}

161
weed/topology/rack.go Normal file
View File

@@ -0,0 +1,161 @@
package topology
import (
"slices"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/util"
)
type Rack struct {
NodeImpl
}
func NewRack(id string) *Rack {
r := &Rack{}
r.id = NodeId(id)
r.nodeType = "Rack"
r.diskUsages = newDiskUsages()
r.children = make(map[NodeId]Node)
r.capacityReservations = newCapacityReservations()
r.NodeImpl.value = r
return r
}
func (r *Rack) FindDataNode(ip string, port int) *DataNode {
for _, c := range r.Children() {
dn := c.(*DataNode)
if dn.MatchLocation(ip, port) {
return dn
}
}
return nil
}
// FindDataNodeById finds a DataNode by its ID using O(1) map lookup
func (r *Rack) FindDataNodeById(id string) *DataNode {
r.RLock()
defer r.RUnlock()
if c, ok := r.children[NodeId(id)]; ok {
return c.(*DataNode)
}
return nil
}
func (r *Rack) GetOrCreateDataNode(ip string, port int, grpcPort int, publicUrl string, id string, maxVolumeCounts map[string]uint32) *DataNode {
r.Lock()
defer r.Unlock()
// Normalize the id parameter (trim whitespace)
id = strings.TrimSpace(id)
// Determine the node ID: use provided id, or fall back to ip:port for backward compatibility
nodeId := util.GetVolumeServerId(id, ip, port)
// First, try to find by node ID using O(1) map lookup (stable identity)
if c, ok := r.children[NodeId(nodeId)]; ok {
dn := c.(*DataNode)
// Log if IP or Port changed (e.g., pod rescheduled in K8s)
addrChanged := dn.Ip != ip || dn.Port != port
var oldAddr pb.ServerAddress
if addrChanged {
oldAddr = dn.ServerAddress()
glog.V(0).Infof("DataNode %s address changed from %s:%d to %s:%d", nodeId, dn.Ip, dn.Port, ip, port)
}
// Update the IP/Port in case they changed
dn.Ip = ip
dn.Port = port
dn.GrpcPort = grpcPort
dn.PublicUrl = publicUrl
dn.LastSeen = time.Now().Unix()
if addrChanged {
if topo := r.GetTopology(); topo != nil {
topo.unregisterDataNodeAddress(oldAddr, dn)
topo.registerDataNodeAddress(dn)
}
}
return dn
}
// For backward compatibility: if explicit id was provided, also check by ip:port
// to handle transition from old (ip:port) to new (explicit id) behavior
ipPortId := util.JoinHostPort(ip, port)
if nodeId != ipPortId {
for oldId, c := range r.children {
dn := c.(*DataNode)
if dn.MatchLocation(ip, port) {
// Only transition if the oldId exactly matches ip:port (legacy identification).
// If oldId is different, this is a node with an explicit id that happens to
// reuse the same ip:port - don't incorrectly merge them.
if string(oldId) != ipPortId {
glog.Warningf("Volume server with id %s has ip:port %s which is used by node %s", nodeId, ipPortId, oldId)
continue
}
// Found a legacy node identified by ip:port, transition it to use the new explicit id
glog.V(0).Infof("Volume server %s transitioning id from %s to %s", dn.Url(), oldId, nodeId)
// Re-key the node in the children map with the new id
delete(r.children, oldId)
dn.id = NodeId(nodeId)
r.children[NodeId(nodeId)] = dn
// Update connection info in case they changed; address itself is
// unchanged on legacy transition, so the index entry stays valid.
dn.GrpcPort = grpcPort
dn.PublicUrl = publicUrl
dn.LastSeen = time.Now().Unix()
return dn
}
}
}
dn := NewDataNode(nodeId)
dn.Ip = ip
dn.Port = port
dn.GrpcPort = grpcPort
dn.PublicUrl = publicUrl
dn.LastSeen = time.Now().Unix()
r.doLinkChildNode(dn)
for diskType, maxVolumeCount := range maxVolumeCounts {
disk := NewDisk(diskType)
disk.diskUsages.getOrCreateDisk(types.ToDiskType(diskType)).maxVolumeCount = int64(maxVolumeCount)
dn.LinkChildNode(disk)
}
return dn
}
type RackInfo struct {
Id NodeId `json:"Id"`
DataNodes []DataNodeInfo `json:"DataNodes"`
}
func (r *Rack) ToInfo() (info RackInfo) {
info.Id = r.Id()
var dns []DataNodeInfo
for _, c := range r.Children() {
dn := c.(*DataNode)
dns = append(dns, dn.ToInfo())
}
slices.SortFunc(dns, func(a, b DataNodeInfo) int {
return strings.Compare(a.Url, b.Url)
})
info.DataNodes = dns
return
}
func (r *Rack) ToRackInfo(filter VolumeFilter) *master_pb.RackInfo {
m := &master_pb.RackInfo{
Id: string(r.Id()),
DiskInfos: r.diskUsages.ToDiskInfo(),
}
for _, c := range r.Children() {
dn := c.(*DataNode)
m.DataNodeInfos = append(m.DataNodeInfos, dn.ToDataNodeInfo(filter))
}
return m
}

View File

@@ -0,0 +1,31 @@
package topology
import (
"context"
"errors"
"testing"
"github.com/seaweedfs/seaweedfs/weed/stats"
)
func TestClassifyReplicationError(t *testing.T) {
tests := []struct {
name string
err error
want string
}{
{"deadline exceeded", context.DeadlineExceeded, stats.FailureTimeout},
{"context cancelled", context.Canceled, stats.FailureContextCancelled},
{"connection refused", errors.New("connection refused"), stats.FailureConnectionRefused},
{"generic error", errors.New("internal server error"), stats.FailureServerError},
{"nil error", nil, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := classifyReplicationError(tt.err)
if got != tt.want {
t.Errorf("classifyReplicationError() = %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,79 @@
package topology
import (
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
// reportedVolumes records which disk types a heartbeat named each volume on, so
// a volume that moved between a server's disks is dropped from the one it left.
// Disk types are interned because a string header per volume would cost more
// than the map holding them.
type reportedVolumes struct {
diskTypes []string
byVolume map[needle.VolumeId]int32
// extra covers volumes named on several disk types, a stale twin rather than
// a move. Nil otherwise.
extra map[needle.VolumeId][]int32
// duplicated: named twice on one disk type, which the master stores once.
duplicated bool
}
func newReportedVolumes(size int) *reportedVolumes {
return &reportedVolumes{byVolume: make(map[needle.VolumeId]int32, size)}
}
func (r *reportedVolumes) add(vid needle.VolumeId, diskType string) {
index := r.internDiskType(diskType)
existing, seen := r.byVolume[vid]
if !seen {
r.byVolume[vid] = index
return
}
if existing == index || r.hasExtra(vid, index) {
r.duplicated = true
return
}
if r.extra == nil {
r.extra = make(map[needle.VolumeId][]int32)
}
r.extra[vid] = append(r.extra[vid], index)
}
func (r *reportedVolumes) internDiskType(diskType string) int32 {
if index := r.diskTypeIndex(diskType); index >= 0 {
return index
}
r.diskTypes = append(r.diskTypes, diskType)
return int32(len(r.diskTypes) - 1)
}
// diskTypeIndex returns -1 when the heartbeat named no volume on the type.
func (r *reportedVolumes) diskTypeIndex(diskType string) int32 {
for i, known := range r.diskTypes {
if known == diskType {
return int32(i)
}
}
return -1
}
func (r *reportedVolumes) namedOn(vid needle.VolumeId, index int32) bool {
if index < 0 {
return false
}
if stored, ok := r.byVolume[vid]; ok && stored == index {
return true
}
return r.hasExtra(vid, index)
}
func (r *reportedVolumes) hasExtra(vid needle.VolumeId, index int32) bool {
for _, other := range r.extra[vid] {
if other == index {
return true
}
}
return false
}
func (r *reportedVolumes) count() int { return len(r.byVolume) }

View File

@@ -0,0 +1,357 @@
package topology
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/operation"
"github.com/seaweedfs/seaweedfs/weed/security"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/util/buffer_pool"
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
"google.golang.org/grpc"
)
// ReplicatedWrite writes a needle to the local volume and fans it out to all
// remote replica locations. When type=replicate is set, the request is itself
// a forwarded replication and no further remote lookups are performed.
// Returns isUnchanged=true when the local write determined the needle content
// was already present.
func ReplicatedWrite(ctx context.Context, masterFn operation.GetMasterFn, grpcDialOption grpc.DialOption, s *storage.Store, volumeId needle.VolumeId, n *needle.Needle, r *http.Request, contentMd5 string) (isUnchanged bool, err error) {
//check JWT
jwt := security.GetJwt(r)
// check whether this is a replicated write request
var remoteLocations []operation.Location
if r.FormValue("type") != "replicate" {
// this is the initial request
remoteLocations, err = GetWritableRemoteReplications(s, grpcDialOption, volumeId, masterFn)
if err != nil {
glog.V(0).Infoln(err)
return
}
}
// read fsync value
fsync := false
if r.FormValue("fsync") == "true" {
fsync = true
}
replicaCount := len(remoteLocations)
if replicaCount > 0 {
// Record replication duration histogram for the overall write operation
defer func(t time.Time) {
stats.VolumeServerReplicationHistogram.WithLabelValues(stats.ReplicationOpWrite).Observe(time.Since(t).Seconds())
}(time.Now())
}
if s.GetVolume(volumeId) != nil {
start := time.Now()
inFlightGauge := stats.VolumeServerInFlightRequestsGauge.WithLabelValues(stats.WriteToLocalDisk)
inFlightGauge.Inc()
defer inFlightGauge.Dec()
isUnchanged, err = s.WriteVolumeNeedle(volumeId, n, true, fsync)
stats.VolumeServerRequestHistogram.WithLabelValues(stats.WriteToLocalDisk).Observe(time.Since(start).Seconds())
if err != nil {
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorWriteToLocalDisk).Inc()
err = fmt.Errorf("failed to write to local disk: %w", err)
glog.V(0).Infoln(err)
return
}
}
// Observe replication targets histogram for all operations (including zero)
stats.VolumeServerReplicationTargets.Observe(float64(replicaCount))
if replicaCount > 0 { //send to other replica locations
start := time.Now()
inFlightGauge := stats.VolumeServerInFlightRequestsGauge.WithLabelValues(stats.WriteToReplicas)
inFlightGauge.Inc()
defer inFlightGauge.Dec()
err = DistributedOperation(ctx, remoteLocations, func(ctx context.Context, location operation.Location) error {
u := url.URL{
Scheme: "http",
Host: location.Url,
Path: r.URL.Path,
}
q := url.Values{
"type": {"replicate"},
"ttl": {n.Ttl.String()},
}
if fsync {
q.Set("fsync", "true")
}
if n.LastModified > 0 {
q.Set("ts", strconv.FormatUint(n.LastModified, 10))
}
if n.IsChunkedManifest() {
q.Set("cm", "true")
}
u.RawQuery = q.Encode()
pairMap := make(map[string]string)
if n.HasPairs() {
tmpMap := make(map[string]string)
err := json.Unmarshal(n.Pairs, &tmpMap)
if err != nil {
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorUnmarshalPairs).Inc()
glog.V(0).Infoln("Unmarshal pairs error:", err)
}
for k, v := range tmpMap {
pairMap[needle.PairNamePrefix+k] = v
}
}
bytesBuffer := buffer_pool.SyncPoolGetBuffer()
defer buffer_pool.SyncPoolPutBuffer(bytesBuffer)
// volume server do not know about encryption
// TODO optimize here to compress data only once
uploadOption := &operation.UploadOption{
UploadUrl: u.String(),
Filename: string(n.Name),
Cipher: false,
IsInputCompressed: n.IsCompressed(),
IsReplication: true,
MimeType: string(n.Mime),
PairMap: pairMap,
Jwt: jwt,
Md5: contentMd5,
BytesBuffer: bytesBuffer,
MaxAttempts: 1, // fail fast on a dead replica; the client write retries
}
uploader, err := operation.NewUploader()
if err != nil {
glog.Errorf("replication-UploadData, err:%v, url:%s", err, u.String())
return err
}
_, err = uploader.UploadData(ctx, n.Data, uploadOption)
if err != nil {
glog.Errorf("replication-UploadData, err:%v, url:%s", err, u.String())
}
return err
})
stats.VolumeServerRequestHistogram.WithLabelValues(stats.WriteToReplicas).Observe(time.Since(start).Seconds())
if err != nil {
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorWriteToReplicas).Inc()
stats.VolumeServerReplicationCounter.WithLabelValues(stats.ReplicationOpWrite, stats.ReplicationFailure).Inc()
reason := classifyReplicationError(err)
stats.VolumeServerReplicationFailures.WithLabelValues(stats.ReplicationOpWrite, reason).Inc()
err = fmt.Errorf("failed to write to replicas for volume %d: %v", volumeId, err)
glog.V(0).Infoln(err)
return false, err
}
stats.VolumeServerReplicationCounter.WithLabelValues(stats.ReplicationOpWrite, stats.ReplicationSuccess).Inc()
}
return
}
// ReplicatedDelete deletes a needle from the local volume and sends delete
// requests to all remote replica locations. Replica deletes use
// context.Background() so that a client disconnect does not orphan replica
// deletes.
func ReplicatedDelete(masterFn operation.GetMasterFn, grpcDialOption grpc.DialOption, store *storage.Store, volumeId needle.VolumeId, n *needle.Needle, r *http.Request) (size types.Size, err error) {
//check JWT
jwt := security.GetJwt(r)
var remoteLocations []operation.Location
if r.FormValue("type") != "replicate" {
remoteLocations, err = GetRemoteReplications(store, grpcDialOption, volumeId, masterFn)
if err != nil {
glog.V(0).Infoln(err)
return
}
}
replicaCount := len(remoteLocations)
if replicaCount > 0 {
// Record replication duration and operation counter for delete
defer func(t time.Time) {
stats.VolumeServerReplicationHistogram.WithLabelValues(stats.ReplicationOpDelete).Observe(time.Since(t).Seconds())
if err != nil {
stats.VolumeServerReplicationCounter.WithLabelValues(stats.ReplicationOpDelete, stats.ReplicationFailure).Inc()
} else {
stats.VolumeServerReplicationCounter.WithLabelValues(stats.ReplicationOpDelete, stats.ReplicationSuccess).Inc()
}
}(time.Now())
}
size, err = store.DeleteVolumeNeedle(volumeId, n)
if err != nil {
glog.V(0).Infoln("delete error:", err)
return
}
// Observe replication targets histogram for all operations (including zero)
stats.VolumeServerReplicationTargets.Observe(float64(replicaCount))
if replicaCount > 0 { //send to other replica locations
// background, not r.Context(): a client disconnect must not orphan replica deletes
if err = DistributedOperation(context.Background(), remoteLocations, func(ctx context.Context, location operation.Location) error {
url, normalizeErr := util_http.GetGlobalHttpClient().NormalizeHttpScheme(location.Url + r.URL.Path + "?type=replicate")
if normalizeErr != nil {
return normalizeErr
}
if jwt != "" && (!strings.HasPrefix(url, "https://") || !util_http.GetGlobalHttpClient().IsTLSVerified()) {
return fmt.Errorf("refusing to forward delete authorization to %s without HTTPS", location.Url)
}
return util_http.Delete(url, string(jwt))
}); err != nil {
reason := classifyReplicationError(err)
stats.VolumeServerReplicationFailures.WithLabelValues(stats.ReplicationOpDelete, reason).Inc()
size = 0
}
}
return
}
type DistributedOperationResult map[string]error
func (dr DistributedOperationResult) Error() error {
var errs []string
for k, v := range dr {
if v != nil {
errs = append(errs, fmt.Sprintf("[%s]: %v", k, v))
}
}
if len(errs) == 0 {
return nil
}
return errors.New(strings.Join(errs, "\n"))
}
type RemoteResult struct {
Host string
Error error
}
func DistributedOperation(ctx context.Context, locations []operation.Location, op func(ctx context.Context, location operation.Location) error) error {
length := len(locations)
if length == 0 {
return nil
}
// cancel outstanding replica ops once the outcome is decided
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// buffered so a straggler (e.g. a replica stalled on a TCP dial timeout) can
// still deliver its result and exit after we have already returned.
resultCh := make(chan RemoteResult, length)
for _, location := range locations {
go func(location operation.Location) {
resultCh <- RemoteResult{location.Url, op(ctx, location)}
}(location)
}
ret := DistributedOperationResult(make(map[string]error))
for i := 0; i < length; i++ {
result := <-resultCh
ret[result.Host] = result.Error
if result.Error != nil {
// fail fast on the first error instead of waiting for slow replicas
return ret.Error()
}
}
return ret.Error()
}
func GetRemoteReplications(s *storage.Store, grpcDialOption grpc.DialOption, volumeId needle.VolumeId, masterFn operation.GetMasterFn) ([]operation.Location, error) {
return getRemoteReplications(s, grpcDialOption, volumeId, masterFn, false)
}
func GetWritableRemoteReplications(s *storage.Store, grpcDialOption grpc.DialOption, volumeId needle.VolumeId, masterFn operation.GetMasterFn) ([]operation.Location, error) {
return getRemoteReplications(s, grpcDialOption, volumeId, masterFn, true)
}
func getRemoteReplications(s *storage.Store, grpcDialOption grpc.DialOption, volumeId needle.VolumeId, masterFn operation.GetMasterFn, writableOnly bool) (remoteLocations []operation.Location, err error) {
v := s.GetVolume(volumeId)
if v != nil && v.ReplicaPlacement.GetCopyCount() == 1 {
return
}
// not on local store, or has replications
lookupResults, lookupErr := operation.LookupVolumeIds(masterFn, grpcDialOption, []string{volumeId.String()}, false)
lookupResult := lookupResults[volumeId.String()]
writableLocations := 0
if lookupErr == nil {
if lookupResult == nil {
err = fmt.Errorf("replicating lookup returned no result for %d", volumeId)
return
}
selfUrl := util.JoinHostPort(s.Ip, s.Port)
for _, location := range lookupResult.Locations {
if writableOnly && location.ReadOnly {
continue
}
if !writableOnly && location.ReadOnly && !location.ReadOnlyCanDelete {
continue
}
writableLocations++
if location.Url != selfUrl {
remoteLocations = append(remoteLocations, location)
}
}
} else {
err = fmt.Errorf("replicating lookup failed for %d: %v", volumeId, lookupErr)
return
}
if v != nil {
// has one local and has remote replications
copyCount := v.ReplicaPlacement.GetCopyCount()
if writableLocations < copyCount {
operation.InvalidateVolumeIdLocationCache(volumeId.String())
label := "replication"
if writableOnly {
label = "writable replication"
}
err = fmt.Errorf("%s locations [%d] is less than volume %d replication copy count [%d]",
label, writableLocations, volumeId, copyCount)
}
}
return
}
// classifyReplicationError maps a Go error to a bounded-cardinality failure
// reason label for the replication_failures_total metric. Returns an empty
// string for nil errors.
func classifyReplicationError(err error) string {
if err == nil {
return ""
}
if errors.Is(err, context.DeadlineExceeded) {
return stats.FailureTimeout
}
if errors.Is(err, context.Canceled) {
return stats.FailureContextCancelled
}
errStr := err.Error()
if strings.Contains(errStr, "connection refused") {
return stats.FailureConnectionRefused
}
return stats.FailureServerError
}

View File

@@ -0,0 +1,227 @@
package topology
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/operation"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// TestDistributedOperationCancelsSiblingsOnFirstError verifies that once one
// replica fails, an outstanding replica still stalled in a dial timeout is
// cancelled rather than gating the caller until it times out.
func TestDistributedOperationCancelsSiblingsOnFirstError(t *testing.T) {
locations := []operation.Location{{Url: "fast"}, {Url: "slow"}}
cancelled := make(chan struct{}, 1)
start := time.Now()
err := DistributedOperation(context.Background(), locations, func(ctx context.Context, location operation.Location) error {
if location.Url == "fast" {
return errors.New("connection refused")
}
// slow: a replica stalled in a dial timeout
select {
case <-ctx.Done():
cancelled <- struct{}{}
return ctx.Err()
case <-time.After(10 * time.Second):
return nil
}
})
if err == nil {
t.Fatal("expected an error from the fast-failing replica")
}
if elapsed := time.Since(start); elapsed > 2*time.Second {
t.Fatalf("did not fail fast: took %v", elapsed)
}
select {
case <-cancelled:
case <-time.After(2 * time.Second):
t.Fatal("slow replica was not cancelled after the first error")
}
}
func TestDistributedOperationEmpty(t *testing.T) {
err := DistributedOperation(context.Background(), nil, func(ctx context.Context, location operation.Location) error {
t.Fatal("op should not be called when there are no locations")
return nil
})
if err != nil {
t.Fatalf("expected nil for no locations, got %v", err)
}
}
type mockMasterServer struct {
master_pb.UnimplementedSeaweedServer
mu sync.Mutex
calls int
locations []*master_pb.Location
}
func (m *mockMasterServer) LookupVolume(ctx context.Context, req *master_pb.LookupVolumeRequest) (*master_pb.LookupVolumeResponse, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.calls++
var vls []*master_pb.LookupVolumeResponse_VolumeIdLocation
for _, vid := range req.VolumeOrFileIds {
vls = append(vls, &master_pb.LookupVolumeResponse_VolumeIdLocation{
VolumeOrFileId: vid,
Locations: m.locations,
})
}
return &master_pb.LookupVolumeResponse{VolumeIdLocations: vls}, nil
}
func startMockMasterServer(t *testing.T, master *mockMasterServer) (operation.GetMasterFn, grpc.DialOption) {
t.Helper()
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
grpcServer := grpc.NewServer()
master_pb.RegisterSeaweedServer(grpcServer, master)
serveErr := make(chan error, 1)
go func() { serveErr <- grpcServer.Serve(lis) }()
t.Cleanup(func() {
grpcServer.Stop()
if err := <-serveErr; err != nil && !errors.Is(err, grpc.ErrServerStopped) {
t.Errorf("mock master serve: %v", err)
}
})
grpcPort := lis.Addr().(*net.TCPAddr).Port
return func(_ context.Context) pb.ServerAddress {
return pb.NewServerAddressWithGrpcPort(fmt.Sprintf("127.0.0.1:%d", grpcPort), grpcPort)
}, grpc.WithTransportCredentials(insecure.NewCredentials())
}
func TestGetWritableRemoteReplicationsRefreshesReadOnlyReplicas(t *testing.T) {
master := &mockMasterServer{locations: []*master_pb.Location{
{Url: "127.0.0.1:8080"},
{Url: "127.0.0.2:8080"},
}}
masterFn, dialOption := startMockMasterServer(t, master)
store := &storage.Store{Ip: "127.0.0.1", Port: 8080}
volumeId := needle.VolumeId(1)
operation.InvalidateVolumeIdLocationCache(volumeId.String())
locations, err := GetWritableRemoteReplications(store, dialOption, volumeId, masterFn)
if err != nil {
t.Fatalf("first lookup: %v", err)
}
if len(locations) != 1 || locations[0].Url != "127.0.0.2:8080" {
t.Fatalf("first lookup locations = %v", locations)
}
master.mu.Lock()
master.locations = []*master_pb.Location{
{Url: "127.0.0.1:8080"},
{Url: "127.0.0.2:8080", ReadOnly: true, ReadOnlyCanDelete: true},
}
master.mu.Unlock()
locations, err = GetWritableRemoteReplications(store, dialOption, volumeId, masterFn)
if err != nil {
t.Fatalf("second lookup: %v", err)
}
if len(locations) != 0 {
t.Fatalf("read-only replica remained a write target: %v", locations)
}
master.mu.Lock()
calls := master.calls
master.mu.Unlock()
if calls != 2 {
t.Fatalf("master lookup calls = %d, want 2", calls)
}
locations, err = GetRemoteReplications(store, dialOption, volumeId, masterFn)
if err != nil {
t.Fatalf("delete lookup: %v", err)
}
if len(locations) != 1 || !locations[0].ReadOnly || !locations[0].ReadOnlyCanDelete {
t.Fatalf("read-only-can-delete target was dropped: %v", locations)
}
}
// TestReplicatedWriteForwardsFsyncToReplicas verifies that the fsync=true
// request parameter is forwarded to replica volume servers in the fan-out
// request, so a durable write means every replica has flushed to disk.
func TestReplicatedWriteForwardsFsyncToReplicas(t *testing.T) {
replicaQueries := make(chan url.Values, 4)
replica := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
replicaQueries <- r.URL.Query()
w.WriteHeader(http.StatusCreated)
if _, err := w.Write([]byte(`{"size":1}`)); err != nil {
t.Errorf("replica write response: %v", err)
}
}))
defer replica.Close()
replicaHost := strings.TrimPrefix(replica.URL, "http://")
masterFn, dialOption := startMockMasterServer(t, &mockMasterServer{
locations: []*master_pb.Location{{Url: replicaHost}},
})
store := &storage.Store{}
volumeId := needle.VolumeId(1)
for _, tc := range []struct {
name string
fsync string
wantFsync bool
}{
{name: "fsync requested", fsync: "true", wantFsync: true},
{name: "no fsync requested", fsync: "", wantFsync: false},
} {
t.Run(tc.name, func(t *testing.T) {
operation.InvalidateVolumeIdLocationCache(volumeId.String())
path := "http://127.0.0.1:8080/1,01637037d6"
if tc.fsync != "" {
path += "?fsync=" + tc.fsync
}
r := httptest.NewRequest(http.MethodPost, path, nil)
if err := r.ParseForm(); err != nil {
t.Fatal(err)
}
n := &needle.Needle{
Id: 1,
Data: []byte("test data"),
Ttl: needle.EMPTY_TTL,
}
if _, err := ReplicatedWrite(context.Background(), masterFn, dialOption, store, volumeId, n, r, ""); err != nil {
t.Fatalf("ReplicatedWrite: %v", err)
}
select {
case q := <-replicaQueries:
got := q.Get("fsync")
if tc.wantFsync && got != "true" {
t.Errorf("expected fsync=true in replica query, got %q (query: %v)", got, q)
}
if !tc.wantFsync && got != "" {
t.Errorf("expected no fsync in replica query, got %q (query: %v)", got, q)
}
case <-time.After(5 * time.Second):
t.Fatal("replica never received the fan-out request")
}
})
}
}

867
weed/topology/topology.go Normal file
View File

@@ -0,0 +1,867 @@
package topology
import (
"encoding/json"
"errors"
"fmt"
"math"
"math/rand/v2"
"slices"
"sync"
"sync/atomic"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
backoff "github.com/cenkalti/backoff/v4"
hashicorpRaft "github.com/hashicorp/raft"
"github.com/seaweedfs/raft"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/util"
)
const (
// WarmupPulseMultiplier is the number of heartbeat intervals to wait after
// a leader change before treating volume lookup misses as definitive.
WarmupPulseMultiplier = 3
)
type Topology struct {
vacuumLockCounter int64
NodeImpl
collectionMap *util.ConcurrentReadMap
ecShardMap map[needle.VolumeId]*EcShardLocations
ecShardMapLock sync.RWMutex
pulse int64
volumeSizeLimit uint64
replicationAsMin bool
vacuumDisabledByOperator atomic.Bool // true when operator manually disables vacuum
vacuumDisabledByPlugin atomic.Bool // true when disabled by the vacuum plugin monitor
adminServerConnectedFunc func() bool // optional callback to check admin server presence
Sequence sequence.Sequencer
chanFullVolumes chan storage.VolumeInfo
chanCrowdedVolumes chan storage.VolumeInfo
Configuration *Configuration
RaftServer raft.Server
RaftServerAccessLock sync.RWMutex
HashicorpRaft *hashicorpRaft.Raft
barrierLock sync.Mutex
barrierDone bool
UuidAccessLock sync.RWMutex
UuidMap map[string][]string
topologyId string
topologyIdLock sync.RWMutex
lastLeaderChangeTime time.Time
hadVolumesAtLeaderChange bool
lastLeaderChangeTimeLock sync.RWMutex
// dataNodeIndex is an address -> *DataNode lookup so callers (e.g. the
// Ping admission gate) do not have to walk every dc/rack/node tier on
// every request. Keys use the canonical http form returned by
// pb.ServerAddress.ToHttpAddress so a target like "1.2.3.4:8080" finds
// the same node whether or not the grpc port suffix is present.
dataNodeIndex map[string]*DataNode
dataNodeIndexLock sync.RWMutex
}
func NewTopology(id string, seq sequence.Sequencer, volumeSizeLimit uint64, pulse int, replicationAsMin bool) *Topology {
t := &Topology{}
t.id = NodeId(id)
t.nodeType = "Topology"
t.NodeImpl.value = t
t.diskUsages = newDiskUsages()
t.children = make(map[NodeId]Node)
t.capacityReservations = newCapacityReservations()
t.collectionMap = util.NewConcurrentReadMap()
t.ecShardMap = make(map[needle.VolumeId]*EcShardLocations)
t.pulse = int64(pulse)
t.volumeSizeLimit = volumeSizeLimit
t.replicationAsMin = replicationAsMin
t.Sequence = seq
t.chanFullVolumes = make(chan storage.VolumeInfo)
t.chanCrowdedVolumes = make(chan storage.VolumeInfo)
t.Configuration = &Configuration{}
t.dataNodeIndex = make(map[string]*DataNode)
return t
}
// LookupDataNodeByAddress returns the registered DataNode that serves addr,
// or nil if no such node has been observed. Lookup is O(1) and uses the
// canonical http form of the address so callers that pass either
// "host:port" or "host:port.grpc" find the same node.
func (t *Topology) LookupDataNodeByAddress(addr pb.ServerAddress) *DataNode {
if addr == "" {
return nil
}
t.dataNodeIndexLock.RLock()
defer t.dataNodeIndexLock.RUnlock()
if t.dataNodeIndex == nil {
return nil
}
return t.dataNodeIndex[addr.ToHttpAddress()]
}
// registerDataNodeAddress records dn in the address index under its current
// http address. Callers must invoke unregisterDataNodeAddress with the prior
// address whenever a node's Ip or Port changes (e.g. k8s pod reschedule).
func (t *Topology) registerDataNodeAddress(dn *DataNode) {
if dn == nil {
return
}
key := dn.ServerAddress().ToHttpAddress()
if key == "" {
return
}
t.dataNodeIndexLock.Lock()
defer t.dataNodeIndexLock.Unlock()
if t.dataNodeIndex == nil {
t.dataNodeIndex = make(map[string]*DataNode)
}
t.dataNodeIndex[key] = dn
}
// unregisterDataNodeAddress removes the index entry for addr, but only when
// the entry still points at dn. The conditional guard avoids dropping a
// freshly re-registered node whose address happens to alias the one being
// removed (e.g. legacy id transitions or a fast restart).
func (t *Topology) unregisterDataNodeAddress(addr pb.ServerAddress, dn *DataNode) {
if addr == "" {
return
}
key := addr.ToHttpAddress()
if key == "" {
return
}
t.dataNodeIndexLock.Lock()
defer t.dataNodeIndexLock.Unlock()
if existing, ok := t.dataNodeIndex[key]; ok && (dn == nil || existing == dn) {
delete(t.dataNodeIndex, key)
}
}
// FreeBytes sums what every volume server reports as free on its filesystems.
// reported is false unless all of them answered: the one that stayed quiet may
// be the one holding the room, and a partial sum would read as a cluster with
// none left.
func (t *Topology) FreeBytes() (freeBytes uint64, reported bool) {
for _, dcNode := range t.Children() {
for _, rackNode := range dcNode.Children() {
for _, dataNode := range rackNode.Children() {
nodeFreeBytes, nodeReported := dataNode.GetDiskUsages().FreeBytes()
if !nodeReported {
return 0, false
}
freeBytes += nodeFreeBytes
}
}
}
return freeBytes, true
}
func (t *Topology) IsChildLocked() (bool, error) {
if t.IsLocked() {
return true, errors.New("topology is locked")
}
for _, dcNode := range t.Children() {
if dcNode.IsLocked() {
return true, fmt.Errorf("topology child %s is locked", dcNode.String())
}
for _, rackNode := range dcNode.Children() {
if rackNode.IsLocked() {
return true, fmt.Errorf("dc %s child %s is locked", dcNode.String(), rackNode.String())
}
for _, dataNode := range rackNode.Children() {
if dataNode.IsLocked() {
return true, fmt.Errorf("rack %s child %s is locked", rackNode.String(), dataNode.Id())
}
}
}
}
return false, nil
}
// SetLastLeaderChangeTime records the time of the most recent leader transition.
// It also snapshots whether the topology already had known volumes at that
// moment. IsWarmingUp uses the snapshot instead of the live MaxVolumeId so a
// fresh cluster that happens to grow its first volume inside the warmup window
// does not retroactively flip into "warming up" state — there is no prior
// topology to wait for on a bootstrap.
func (t *Topology) SetLastLeaderChangeTime(ts time.Time) {
hadVolumes := t.GetMaxVolumeId() > 0
t.lastLeaderChangeTimeLock.Lock()
defer t.lastLeaderChangeTimeLock.Unlock()
t.lastLeaderChangeTime = ts
t.hadVolumesAtLeaderChange = hadVolumes
}
// GetLastLeaderChangeTime returns the time of the most recent leader transition.
func (t *Topology) GetLastLeaderChangeTime() time.Time {
t.lastLeaderChangeTimeLock.RLock()
defer t.lastLeaderChangeTimeLock.RUnlock()
return t.lastLeaderChangeTime
}
// IsWarmingUp returns true if the master recently became leader and may not yet
// have a complete topology. After a leader change or restart, volume servers need
// up to WarmupPulseMultiplier heartbeat intervals to reconnect and report their volumes.
// Returns false on a fresh cluster start — i.e. when no volumes existed at the
// time of the leader change — since there is no prior topology state to wait for.
// Checking the *live* MaxVolumeId here would make a bootstrapping cluster flip
// into warming-up the moment its first volume is grown, which manifested as a
// 15-second window of spurious Unavailable errors on AssignVolume for workloads
// that start writing immediately (see #8777).
func (t *Topology) IsWarmingUp() bool {
t.lastLeaderChangeTimeLock.RLock()
lastChange := t.lastLeaderChangeTime
hadVolumes := t.hadVolumesAtLeaderChange
t.lastLeaderChangeTimeLock.RUnlock()
if !hadVolumes || lastChange.IsZero() {
return false
}
return time.Since(lastChange) < t.WarmupDuration()
}
// WarmupDuration returns the configured warmup duration based on pulse interval.
func (t *Topology) WarmupDuration() time.Duration {
return time.Duration(t.pulse*WarmupPulseMultiplier) * time.Second
}
// RemainingWarmupDuration returns how much warmup time is left, or 0 if not warming up.
func (t *Topology) RemainingWarmupDuration() time.Duration {
if !t.IsWarmingUp() {
return 0
}
remaining := t.WarmupDuration() - time.Since(t.GetLastLeaderChangeTime())
if remaining < 0 {
return 0
}
return remaining
}
func (t *Topology) IsLeader() bool {
t.RaftServerAccessLock.RLock()
defer t.RaftServerAccessLock.RUnlock()
if t.RaftServer != nil {
if t.RaftServer.State() == raft.Leader {
return true
}
// Directly check leader to avoid re-acquiring lock via MaybeLeader()
leader := pb.ServerAddress(t.RaftServer.Leader())
if leader != "" {
if pb.ServerAddress(t.RaftServer.Name()).Equals(leader) {
return true
}
}
} else if t.HashicorpRaft != nil {
if t.HashicorpRaft.State() == hashicorpRaft.Leader {
return true
}
}
return false
}
func (t *Topology) IsLeaderAndCanRead() bool {
if t.RaftServer != nil {
return t.IsLeader()
} else if t.HashicorpRaft != nil {
return t.IsLeader() && t.DoBarrier()
} else {
return false
}
}
func (t *Topology) DoBarrier() bool {
t.barrierLock.Lock()
defer t.barrierLock.Unlock()
if t.barrierDone {
return true
}
glog.V(0).Infof("raft do barrier")
barrier := t.HashicorpRaft.Barrier(2 * time.Minute)
if err := barrier.Error(); err != nil {
glog.Errorf("failed to wait for barrier, error %s", err)
return false
}
t.barrierDone = true
glog.V(0).Infof("raft do barrier success")
return true
}
func (t *Topology) BarrierReset() {
t.barrierLock.Lock()
defer t.barrierLock.Unlock()
t.barrierDone = false
}
func (t *Topology) Leader() (l pb.ServerAddress, err error) {
exponentialBackoff := backoff.NewExponentialBackOff()
exponentialBackoff.InitialInterval = 100 * time.Millisecond
exponentialBackoff.MaxElapsedTime = 20 * time.Second
leaderNotSelected := errors.New("leader not selected yet")
l, err = backoff.RetryWithData(
func() (l pb.ServerAddress, err error) {
l, err = t.MaybeLeader()
if err == nil && l == "" {
err = leaderNotSelected
}
return l, err
},
exponentialBackoff)
if err == leaderNotSelected {
l = ""
}
return l, err
}
func (t *Topology) MaybeLeader() (l pb.ServerAddress, err error) {
t.RaftServerAccessLock.RLock()
defer t.RaftServerAccessLock.RUnlock()
if t.RaftServer != nil {
l = pb.ServerAddress(t.RaftServer.Leader())
if l == "" && t.RaftServer.State() == raft.Leader {
l = pb.ServerAddress(t.RaftServer.Name())
}
} else if t.HashicorpRaft != nil {
l = pb.ServerAddress(t.HashicorpRaft.Leader())
} else {
err = errors.New("Raft Server not ready yet!")
}
return
}
func (t *Topology) Lookup(collection string, vid needle.VolumeId) (dataNodes []*DataNode) {
// maybe an issue if lots of collections?
if collection == "" {
for _, c := range t.collectionMap.Items() {
if list := c.(*Collection).Lookup(vid); list != nil {
return list
}
}
} else {
if c, ok := t.collectionMap.Find(collection); ok {
return c.(*Collection).Lookup(vid)
}
}
if locations, found := t.LookupEcShards(vid); found {
for _, loc := range locations.Locations {
dataNodes = append(dataNodes, loc...)
}
return dataNodes
}
return nil
}
func (t *Topology) NextVolumeId() (needle.VolumeId, error) {
if !t.IsLeaderAndCanRead() {
return 0, fmt.Errorf("as leader can not read yet")
}
vid := t.GetMaxVolumeId()
next := vid.Next()
t.RaftServerAccessLock.RLock()
defer t.RaftServerAccessLock.RUnlock()
if t.RaftServer != nil {
if _, err := t.RaftServer.Do(NewMaxVolumeIdCommand(next, t.GetTopologyId())); err != nil {
return 0, err
}
} else if t.HashicorpRaft != nil {
b, err := json.Marshal(NewMaxVolumeIdCommand(next, t.GetTopologyId()))
if err != nil {
return 0, fmt.Errorf("failed marshal NewMaxVolumeIdCommand: %+v", err)
}
if future := t.HashicorpRaft.Apply(b, time.Second); future.Error() != nil {
return 0, future.Error()
}
}
return next, nil
}
// DefaultNeedleSizeEstimate is the fallback per-file-ID size estimate when
// the client does not provide an expected data size.
const DefaultNeedleSizeEstimate uint64 = 1024 * 1024 // 1 MB
func (t *Topology) PickForWrite(requestedCount uint64, option *VolumeGrowOption, volumeLayout *VolumeLayout, expectedDataSize uint64) (fileId string, count uint64, volumeLocationList *VolumeLocationList, shouldGrow bool, err error) {
var vid needle.VolumeId
vid, count, volumeLocationList, shouldGrow, err = volumeLayout.PickForWrite(requestedCount, option)
if err != nil {
return "", 0, nil, shouldGrow, fmt.Errorf("failed to find writable volumes for collection:%s replication:%s ttl:%s error: %v", option.Collection, option.ReplicaPlacement.String(), option.Ttl.String(), err)
}
if volumeLocationList == nil || volumeLocationList.Length() == 0 {
return "", 0, nil, shouldGrow, fmt.Errorf("%s available for collection:%s replication:%s ttl:%s", NoWritableVolumes, option.Collection, option.ReplicaPlacement.String(), option.Ttl.String())
}
// Track estimated assigned bytes to spread load between heartbeats. A flat
// fallback overcharges a small-file workload enough to mark near-empty
// volumes full, so prefer the volume's own average.
sizePerFile := DefaultNeedleSizeEstimate
if expectedDataSize > 0 {
sizePerFile = expectedDataSize
} else if vi, infoErr := volumeLocationList.Head().GetVolumesById(vid); infoErr == nil && vi.FileCount > 0 {
if avg := vi.Size / uint64(vi.FileCount); avg > 0 {
sizePerFile = avg
}
}
pendingBytes := min(uint64(count)*sizePerFile, uint64(math.MaxInt64))
if volumeLayout.RecordAssign(vid, int64(pendingBytes)) {
volumeLayout.AdjustActiveVolumeCountForFull(vid)
}
nextFileId := t.Sequence.NextFileId(requestedCount)
fileId = needle.NewFileId(vid, nextFileId, rand.Uint32()).String()
return fileId, count, volumeLocationList, shouldGrow, nil
}
func (t *Topology) GetVolumeLayout(collectionName string, rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType) *VolumeLayout {
return t.collectionMap.Get(collectionName, func() interface{} {
return NewCollection(collectionName, t.volumeSizeLimit, t.replicationAsMin)
}).(*Collection).GetOrCreateVolumeLayout(rp, ttl, diskType)
}
// DecayQuietVolumeSizes decays pending assign estimates across every layout.
// A volume that changed reports within a pulse, so two quiet pulses mean the
// size on record is the size there is.
func (t *Topology) DecayQuietVolumeSizes() {
quietCutoff := time.Duration(2*t.pulse) * time.Second
for _, c := range t.collectionMap.Items() {
for _, vl := range c.(*Collection).GetAllVolumeLayouts() {
vl.DecayQuietVolumeSizes(quietCutoff)
}
}
}
// CollectionVolumeStats aggregates stats across all volume layouts and EC
// volumes of one collection, or across every collection when collectionName is
// empty.
func (t *Topology) CollectionVolumeStats(collectionName string) *VolumeLayoutStats {
ret := &VolumeLayoutStats{}
var collections []*Collection
if collectionName == "" {
for _, c := range t.collectionMap.Items() {
collections = append(collections, c.(*Collection))
}
} else if c, found := t.FindCollection(collectionName); found {
collections = append(collections, c)
}
for _, c := range collections {
for _, vl := range c.GetAllVolumeLayouts() {
stats := vl.Stats()
ret.TotalSize += stats.TotalSize
ret.UsedSize += stats.UsedSize
ret.LogicalUsedSize += stats.LogicalUsedSize
ret.FileCount += stats.FileCount
}
}
// EC volumes live outside collectionMap, so a collection whose volumes are
// all encoded has no layout left to report them
ecStats := t.CollectionEcVolumeStats(collectionName)
ret.TotalSize += ecStats.TotalSize
ret.UsedSize += ecStats.UsedSize
ret.LogicalUsedSize += ecStats.LogicalUsedSize
ret.FileCount += ecStats.FileCount
return ret
}
func (t *Topology) ListCollections(includeNormalVolumes, includeEcVolumes bool) (ret []string) {
found := make(map[string]bool)
if includeNormalVolumes {
t.collectionMap.RLock()
for _, c := range t.collectionMap.Items() {
found[c.(*Collection).Name] = true
}
t.collectionMap.RUnlock()
}
if includeEcVolumes {
t.ecShardMapLock.RLock()
for _, ecVolumeLocation := range t.ecShardMap {
found[ecVolumeLocation.Collection] = true
}
t.ecShardMapLock.RUnlock()
}
for k := range found {
ret = append(ret, k)
}
slices.Sort(ret)
return ret
}
func (t *Topology) FindCollection(collectionName string) (*Collection, bool) {
c, hasCollection := t.collectionMap.Find(collectionName)
if !hasCollection {
return nil, false
}
return c.(*Collection), hasCollection
}
func (t *Topology) DeleteCollection(collectionName string) {
// The layouts vanish with the collection, but every location they served
// holds a bit in its node's lookup digest. Left in place, those bits keep
// the node's held and servable digests apart forever, and the master asks
// for the full volume list on every heartbeat from then on.
// Unpublish first so a racing registration re-resolves into a fresh collection.
collection, found := t.collectionMap.Delete(collectionName)
if !found {
return
}
for _, vl := range collection.(*Collection).GetAllVolumeLayouts() {
vl.releaseLookupOwnership()
}
}
func (t *Topology) DeleteLayout(collectionName string, rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType) {
collection, found := t.FindCollection(collectionName)
if !found {
return
}
collection.DeleteVolumeLayout(rp, ttl, diskType)
if len(collection.storageType2VolumeLayout.Items()) == 0 {
t.DeleteCollection(collectionName)
}
}
func (t *Topology) RegisterVolumeLayout(v storage.VolumeInfo, dn *DataNode) {
diskType := types.ToDiskType(v.DiskType)
for {
vl := t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
if vl.RegisterVolume(&v, dn) {
vl.EnsureCorrectWritables(&v)
return
}
// Dropped with its collection; the next lookup creates a fresh one.
}
}
func (t *Topology) UnRegisterVolumeLayout(v storage.VolumeInfo, dn *DataNode) {
glog.Infof("removing volume info: %+v from %v", v, dn.id)
diskType := types.ToDiskType(v.DiskType)
volumeLayout := t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
volumeLayout.UnRegisterVolume(&v, dn)
// Drop the series only after the last placement is gone. Deleting while
// another data node still holds v would hide under-replication until the
// next CollectDeadNodeAndFullVolumes cycle recreates the label.
if v.ReplicaPlacement.GetCopyCount() > 1 && len(t.Lookup(v.Collection, v.Id)) == 0 {
stats.MasterReplicaPlacementMismatch.DeleteLabelValues(v.Collection, v.Id.String())
}
if volumeLayout.isEmpty() {
t.DeleteLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
}
}
func (t *Topology) DataCenterExists(dcName string) bool {
return dcName == "" || t.GetDataCenter(dcName) != nil
}
func (t *Topology) GetDataCenter(dcName string) (dc *DataCenter) {
t.RLock()
defer t.RUnlock()
for _, c := range t.children {
dc = c.(*DataCenter)
if string(dc.Id()) == dcName {
return dc
}
}
return dc
}
func (t *Topology) GetOrCreateDataCenter(dcName string) *DataCenter {
t.Lock()
defer t.Unlock()
for _, c := range t.children {
dc := c.(*DataCenter)
if string(dc.Id()) == dcName {
return dc
}
}
dc := NewDataCenter(dcName)
t.doLinkChildNode(dc)
return dc
}
func (t *Topology) ListDataCenters() (dcs []string) {
t.RLock()
defer t.RUnlock()
for _, c := range t.children {
dcs = append(dcs, string(c.(*DataCenter).Id()))
}
return dcs
}
func (t *Topology) ListDCAndRacks() (dcs map[NodeId][]NodeId) {
t.RLock()
defer t.RUnlock()
dcs = make(map[NodeId][]NodeId)
for _, dcNode := range t.children {
dcNodeId := dcNode.(*DataCenter).Id()
for _, rackNode := range dcNode.Children() {
dcs[dcNodeId] = append(dcs[dcNodeId], rackNode.(*Rack).Id())
}
}
return dcs
}
func (t *Topology) SyncDataNodeRegistration(volumes []*master_pb.VolumeInformationMessage, dn *DataNode) (newVolumes, deletedVolumes, changedVolumes []storage.VolumeInfo) {
// convert into in memory struct storage.VolumeInfo
volumeInfos := make([]storage.VolumeInfo, 0, len(volumes))
for _, v := range volumes {
if vi, err := storage.NewVolumeInfo(v); err == nil {
volumeInfos = append(volumeInfos, vi)
} else {
glog.V(0).Infof("Fail to convert joined volume information: %v", err)
}
}
// find out the delta volumes
newVolumes, deletedVolumes, changedVolumes = dn.UpdateVolumes(volumeInfos)
for _, v := range newVolumes {
t.RegisterVolumeLayout(v, dn)
}
for _, v := range deletedVolumes {
t.UnRegisterVolumeLayout(v, dn)
}
// Update effective sizes for all reported volumes (decay pending estimates).
// If decay brings a volume eagerly removed by RecordAssign back under the
// writable threshold, restore the matching activeVolumeCount.
for _, v := range volumeInfos {
if v.ReplicaPlacement == nil {
continue
}
diskType := types.ToDiskType(v.DiskType)
vl := t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
// Self-heal: a volume reported by the data node but missing from the
// lookup index is re-registered. This repairs the split left by a
// disconnect/reconnect race, where UnRegisterDataNode dropped the volume
// from vid2location but the reconnecting full heartbeat skipped it
// (still in the disk map, so UpdateVolumes did not report it as new).
// Without this, the volume stays visible in volume.list/admin UI yet
// LookupVolume returns "volume id not found".
if !vl.HasDataNode(v.Id, dn) {
for !vl.RegisterVolume(&v, dn) {
// Dropped with its collection; the next lookup creates a fresh
// one, which the calls below must use too.
vl = t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
}
// Volumes new to the disk map were registered above, so reaching
// here means only the lookup index had lost it. Clients were told
// it went when the node dropped out, so the repair has to tell them
// it is back.
newVolumes = append(newVolumes, v)
}
vl.UpdateOversizedState(&v, dn)
if vl.UpdateVolumeSize(v.Id, v.Size, v.CompactRevision, true) {
vl.AdjustActiveVolumeCountAfterRecovery(v.Id)
}
vl.EnsureCorrectWritables(&v)
}
return
}
func (t *Topology) IncrementalSyncDataNodeRegistration(newVolumes, deletedVolumes []*master_pb.VolumeShortInformationMessage, dn *DataNode) {
var newVis, oldVis []storage.VolumeInfo
for _, v := range newVolumes {
vi, err := storage.NewVolumeInfoFromShort(v)
if err != nil {
glog.V(0).Infof("NewVolumeInfoFromShort %v: %v", v, err)
continue
}
newVis = append(newVis, vi)
}
for _, v := range deletedVolumes {
vi, err := storage.NewVolumeInfoFromShort(v)
if err != nil {
glog.V(0).Infof("NewVolumeInfoFromShort %v: %v", v, err)
continue
}
oldVis = append(oldVis, vi)
}
dn.DeltaUpdateVolumes(newVis, oldVis)
type layoutKey struct {
id needle.VolumeId
collection string
replicaPlacement byte
ttl uint32
diskType types.DiskType
}
key := func(vi storage.VolumeInfo) layoutKey {
return layoutKey{
id: vi.Id,
collection: vi.Collection,
replicaPlacement: vi.ReplicaPlacement.Byte(),
ttl: vi.Ttl.ToUint32(),
diskType: types.ToDiskType(vi.DiskType),
}
}
replacements := make(map[layoutKey]struct{}, len(newVis))
for _, vi := range newVis {
replacements[key(vi)] = struct{}{}
t.RegisterVolumeLayout(vi, dn)
}
for _, oldVi := range oldVis {
if _, replaced := replacements[key(oldVi)]; !replaced {
t.UnRegisterVolumeLayout(oldVi, dn)
}
}
return
}
// ApplyVolumeChanges records the volumes a heartbeat reported as changed and
// returns the ones the node did not already have. Only the named volumes are
// touched: unlike a full report, silence about a volume says nothing about
// whether the server still has it.
//
// Most changes are a volume growing, which moves no location, so returning
// only the arrivals keeps a busy cluster from telling every client about
// volumes they can already reach. The arrival set also includes replicas whose
// IsRemote() classification flipped on tier transition: the volume is still
// servable from the same node, but every connected client has stale replica
// priority and must be told to refresh.
func (t *Topology) ApplyVolumeChanges(changed []*master_pb.VolumeInformationMessage, dn *DataNode) (newVolumes []storage.VolumeInfo) {
volumeInfos := make([]storage.VolumeInfo, 0, len(changed))
for _, v := range changed {
vi, err := storage.NewVolumeInfo(v)
if err != nil {
glog.V(0).Infof("Fail to convert changed volume information: %v", err)
continue
}
volumeInfos = append(volumeInfos, vi)
}
for _, vi := range volumeInfos {
isNew, isChanged, tierTransition := dn.AddOrUpdateVolume(vi)
if vi.ReplicaPlacement == nil {
if isNew {
newVolumes = append(newVolumes, vi)
}
continue
}
vl := t.GetVolumeLayout(vi.Collection, vi.ReplicaPlacement, vi.Ttl, types.ToDiskType(vi.DiskType))
// Reaching the lookup index is what makes a volume servable, so a
// volume only that index had lost is an arrival as far as clients are
// concerned: they were told it went when the node dropped out.
becameServable := !vl.HasDataNode(vi.Id, dn)
for becameServable && !vl.RegisterVolume(&vi, dn) {
// Dropped with its collection; the next lookup creates a fresh one.
vl = t.GetVolumeLayout(vi.Collection, vi.ReplicaPlacement, vi.Ttl, types.ToDiskType(vi.DiskType))
}
if isNew || becameServable || tierTransition || isChanged {
newVolumes = append(newVolumes, vi)
}
vl.UpdateOversizedState(&vi, dn)
if vl.UpdateVolumeSize(vi.Id, vi.Size, vi.CompactRevision, true) {
vl.AdjustActiveVolumeCountAfterRecovery(vi.Id)
}
vl.EnsureCorrectWritables(&vi)
}
return newVolumes
}
func (t *Topology) DataNodeRegistration(dcName, rackName string, dn *DataNode) {
if dn.Parent() != nil {
return
}
// registration to topo
dc := t.GetOrCreateDataCenter(dcName)
rack := dc.GetOrCreateRack(rackName)
rack.LinkChildNode(dn)
glog.Infof("[%s] reLink To topo ", dn.Id())
}
// IsVacuumDisabled returns true if vacuum is disabled by either the
// operator or the plugin monitor.
func (t *Topology) IsVacuumDisabled() bool {
return t.vacuumDisabledByOperator.Load() || t.vacuumDisabledByPlugin.Load()
}
// DisableVacuum is called by the operator (shell command / manual RPC).
// Only sets the operator flag; does not affect the plugin flag.
func (t *Topology) DisableVacuum() {
glog.V(0).Infof("DisableVacuum (by operator)")
t.vacuumDisabledByOperator.Store(true)
}
// EnableVacuum is called by the operator (shell command / manual RPC).
// Only clears the operator flag; does not affect the plugin flag.
func (t *Topology) EnableVacuum() {
glog.V(0).Infof("EnableVacuum (by operator)")
t.vacuumDisabledByOperator.Store(false)
}
// DisableVacuumByPlugin is called by the admin server's vacuum monitor
// when a vacuum plugin worker connects. Only sets the plugin flag.
func (t *Topology) DisableVacuumByPlugin() {
glog.V(0).Infof("DisableVacuum (by plugin worker)")
t.vacuumDisabledByPlugin.Store(true)
}
// EnableVacuumByPlugin is called by the admin server's vacuum monitor
// when a vacuum plugin worker disconnects. Only clears the plugin flag.
func (t *Topology) EnableVacuumByPlugin() {
glog.V(0).Infof("EnableVacuum (by plugin worker)")
t.vacuumDisabledByPlugin.Store(false)
}
// IsVacuumDisabledByPlugin returns whether the plugin monitor has disabled vacuum.
func (t *Topology) IsVacuumDisabledByPlugin() bool {
return t.vacuumDisabledByPlugin.Load()
}
// SetAdminServerConnectedFunc sets an optional callback used by the vacuum
// safety net to detect when the admin server has disconnected.
func (t *Topology) SetAdminServerConnectedFunc(f func() bool) {
t.adminServerConnectedFunc = f
}
func (t *Topology) GetTopologyId() string {
t.topologyIdLock.RLock()
defer t.topologyIdLock.RUnlock()
return t.topologyId
}
func (t *Topology) SetTopologyId(topologyId string) {
t.topologyIdLock.Lock()
defer t.topologyIdLock.Unlock()
if topologyId == "" {
return
}
if t.topologyId == "" {
t.topologyId = topologyId
return
}
if t.topologyId != topologyId {
glog.Fatalf("Split-brain detected! Current TopologyId is %s, but received %s. Stopping to prevent data corruption.", t.topologyId, topologyId)
}
}

View File

@@ -0,0 +1,281 @@
package topology
import (
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
type EcShardLocations struct {
Collection string
// Use MaxShardCount (32) to support custom EC ratios
Locations [erasure_coding.MaxShardCount][]*DataNode
}
func (t *Topology) SyncDataNodeEcShards(shardInfos []*master_pb.VolumeEcShardInformationMessage, dn *DataNode) (newShards, deletedShards []*erasure_coding.EcVolumeInfo) {
// convert into in memory struct storage.VolumeInfo
var shards []*erasure_coding.EcVolumeInfo
for _, shardInfo := range shardInfos {
// Create EcVolumeInfo directly with optimized format
ecVolumeInfo := &erasure_coding.EcVolumeInfo{
VolumeId: needle.VolumeId(shardInfo.Id),
Collection: shardInfo.Collection,
ShardsInfo: erasure_coding.ShardsInfoFromVolumeEcShardInformationMessage(shardInfo),
DiskType: shardInfo.DiskType,
DiskId: shardInfo.DiskId,
ExpireAtSec: shardInfo.ExpireAtSec,
FileCount: shardInfo.FileCount,
DeleteCount: shardInfo.DeleteCount,
EncodeTsNs: shardInfo.EncodeTsNs,
}
shards = append(shards, ecVolumeInfo)
}
// find out the delta volumes
newShards, deletedShards = dn.UpdateEcShards(shards)
for _, v := range newShards {
t.RegisterEcShards(v, dn)
}
for _, v := range deletedShards {
t.UnRegisterEcShards(v, dn)
}
return
}
func (t *Topology) IncrementalSyncDataNodeEcShards(newEcShards, deletedEcShards []*master_pb.VolumeEcShardInformationMessage, dn *DataNode) {
// convert into in memory struct storage.VolumeInfo
var newShards, deletedShards []*erasure_coding.EcVolumeInfo
for _, shardInfo := range newEcShards {
// Create EcVolumeInfo directly with optimized format
ecVolumeInfo := &erasure_coding.EcVolumeInfo{
VolumeId: needle.VolumeId(shardInfo.Id),
Collection: shardInfo.Collection,
ShardsInfo: erasure_coding.ShardsInfoFromVolumeEcShardInformationMessage(shardInfo),
DiskType: shardInfo.DiskType,
DiskId: shardInfo.DiskId,
ExpireAtSec: shardInfo.ExpireAtSec,
FileCount: shardInfo.FileCount,
DeleteCount: shardInfo.DeleteCount,
EncodeTsNs: shardInfo.EncodeTsNs,
}
newShards = append(newShards, ecVolumeInfo)
}
for _, shardInfo := range deletedEcShards {
// Create EcVolumeInfo directly with optimized format
ecVolumeInfo := &erasure_coding.EcVolumeInfo{
VolumeId: needle.VolumeId(shardInfo.Id),
Collection: shardInfo.Collection,
ShardsInfo: erasure_coding.ShardsInfoFromVolumeEcShardInformationMessage(shardInfo),
DiskType: shardInfo.DiskType,
DiskId: shardInfo.DiskId,
ExpireAtSec: shardInfo.ExpireAtSec,
FileCount: shardInfo.FileCount,
DeleteCount: shardInfo.DeleteCount,
EncodeTsNs: shardInfo.EncodeTsNs,
}
deletedShards = append(deletedShards, ecVolumeInfo)
}
dn.DeltaUpdateEcShards(newShards, deletedShards)
for _, v := range newShards {
t.RegisterEcShards(v, dn)
}
for _, v := range deletedShards {
t.UnRegisterEcShards(v, dn)
}
}
func NewEcShardLocations(collection string) *EcShardLocations {
return &EcShardLocations{
Collection: collection,
}
}
func (loc *EcShardLocations) AddShard(shardId erasure_coding.ShardId, dn *DataNode) (added bool) {
// Defensive bounds check to prevent panic with out-of-range shard IDs
if int(shardId) >= erasure_coding.MaxShardCount {
return false
}
dataNodes := loc.Locations[shardId]
for _, n := range dataNodes {
if n.Id() == dn.Id() {
return false
}
}
loc.Locations[shardId] = append(dataNodes, dn)
return true
}
func (loc *EcShardLocations) DeleteShard(shardId erasure_coding.ShardId, dn *DataNode) (deleted bool) {
// Defensive bounds check to prevent panic with out-of-range shard IDs
if int(shardId) >= erasure_coding.MaxShardCount {
return false
}
dataNodes := loc.Locations[shardId]
foundIndex := -1
for index, n := range dataNodes {
if n.Id() == dn.Id() {
foundIndex = index
}
}
if foundIndex < 0 {
return false
}
loc.Locations[shardId] = append(dataNodes[:foundIndex], dataNodes[foundIndex+1:]...)
return true
}
func (t *Topology) RegisterEcShards(ecvi *erasure_coding.EcVolumeInfo, dn *DataNode) {
// EC-only volumes (source volume deleted after encoding) must bump
// maxVolumeId too, or a heartbeat-rebuilt master could re-issue their id.
t.UpAdjustMaxVolumeId(ecvi.VolumeId)
t.ecShardMapLock.Lock()
defer t.ecShardMapLock.Unlock()
locations, found := t.ecShardMap[ecvi.VolumeId]
if !found {
locations = NewEcShardLocations(ecvi.Collection)
t.ecShardMap[ecvi.VolumeId] = locations
}
for _, shardId := range ecvi.ShardsInfo.Ids() {
locations.AddShard(shardId, dn)
}
}
func (t *Topology) UnRegisterEcShards(ecvi *erasure_coding.EcVolumeInfo, dn *DataNode) {
glog.Infof("removing ec shard info:%+v", ecvi)
t.ecShardMapLock.Lock()
defer t.ecShardMapLock.Unlock()
locations, found := t.ecShardMap[ecvi.VolumeId]
if !found {
return
}
for _, shardId := range ecvi.ShardsInfo.Ids() {
locations.DeleteShard(shardId, dn)
}
}
func (t *Topology) LookupEcShards(vid needle.VolumeId) (locations *EcShardLocations, found bool) {
t.ecShardMapLock.RLock()
defer t.ecShardMapLock.RUnlock()
locations, found = t.ecShardMap[vid]
return
}
// ecVolumeCounts accumulates one EC volume's data size and needle counts while
// they are collected from every node reporting its shards.
type ecVolumeCounts struct {
countedShards erasure_coding.ShardBits
dataSize uint64
fileCount uint64
deleteCount uint64
}
// CollectionEcVolumeStats sums the disk footprint and live needle count of the
// EC volumes in one collection, or in every collection when collectionName is
// empty. Every shard copy counts, parity included, the way a regular volume's
// used size counts every replica; needle counts are per volume, again as a
// regular volume reports them. The logical size instead counts each volume's
// data shards once, leaving out parity and over-replicated copies.
func (t *Topology) CollectionEcVolumeStats(collectionName string) *VolumeLayoutStats {
ret := &VolumeLayoutStats{}
perVolume := make(map[needle.VolumeId]*ecVolumeCounts)
for _, c := range t.Children() {
for _, r := range c.(*DataCenter).Children() {
for _, n := range r.(*Rack).Children() {
for _, ecInfo := range n.(*DataNode).GetEcShards() {
if collectionName != "" && ecInfo.Collection != collectionName {
continue
}
ret.UsedSize += uint64(ecInfo.ShardsInfo.TotalSize())
counts, found := perVolume[ecInfo.VolumeId]
if !found {
counts = &ecVolumeCounts{}
perVolume[ecInfo.VolumeId] = counts
}
dataShards := ecInfo.DataShardsOrDefault()
for id := range erasure_coding.ShardBits(ecInfo.ShardsInfo.Bitmap()).All() {
if int(id) >= dataShards || counts.countedShards.Has(id) {
continue
}
counts.countedShards = counts.countedShards.Set(id)
counts.dataSize += uint64(ecInfo.ShardsInfo.Size(id))
}
// .ecx and .ecj are both volume-wide files that travel with
// the shards, so take the largest count any holder reports
// rather than summing: a node still loading .ecx reports 0
// and must not pin the total down, and a shard move copies
// the journal, so several holders can report the same
// tombstones. Deletes recorded only on another holder since
// then are missed, which errs toward reporting files that
// are gone rather than losing a whole volume's count.
if ecInfo.FileCount > counts.fileCount {
counts.fileCount = ecInfo.FileCount
}
if ecInfo.DeleteCount > counts.deleteCount {
counts.deleteCount = ecInfo.DeleteCount
}
}
}
}
}
// an EC volume is sealed, so it offers no room beyond what it holds
ret.TotalSize = ret.UsedSize
for _, counts := range perVolume {
ret.LogicalUsedSize += counts.dataSize
if counts.fileCount > counts.deleteCount {
ret.FileCount += counts.fileCount - counts.deleteCount
}
}
return ret
}
func (t *Topology) ListEcServersByCollection(collection string) (dataNodes []pb.ServerAddress) {
t.ecShardMapLock.RLock()
defer t.ecShardMapLock.RUnlock()
dateNodeMap := make(map[pb.ServerAddress]bool)
for _, ecVolumeLocation := range t.ecShardMap {
if ecVolumeLocation.Collection == collection {
for _, locations := range ecVolumeLocation.Locations {
for _, loc := range locations {
dateNodeMap[loc.ServerAddress()] = true
}
}
}
}
for k, _ := range dateNodeMap {
dataNodes = append(dataNodes, k)
}
return
}
func (t *Topology) DeleteEcCollection(collection string) {
t.ecShardMapLock.Lock()
defer t.ecShardMapLock.Unlock()
var vids []needle.VolumeId
for vid, ecVolumeLocation := range t.ecShardMap {
if ecVolumeLocation.Collection == collection {
vids = append(vids, vid)
}
}
for _, vid := range vids {
delete(t.ecShardMap, vid)
}
}

View File

@@ -0,0 +1,50 @@
package topology
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
// TestRegisterEcShardsAdvancesMaxVolumeId guards against volume-id reuse: an
// EC-only volume must advance maxVolumeId so a heartbeat-rebuilt master cannot
// re-issue its id.
func TestRegisterEcShardsAdvancesMaxVolumeId(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 100})
const ecVid = uint32(39848)
if got := topo.GetMaxVolumeId(); uint32(got) >= ecVid {
t.Fatalf("precondition: maxVolumeId already %d, want < %d", got, ecVid)
}
// Full-sync heartbeat reporting an EC-only volume.
msg := buildEcShardMessage(ecVid, "prefeitura", "", 0, []erasure_coding.ShardId{0, 1, 2, 3})
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{msg}, dn)
if got := topo.GetMaxVolumeId(); got != needle.VolumeId(ecVid) {
t.Fatalf("maxVolumeId after EC registration = %d, want %d", got, ecVid)
}
}
// TestIncrementalSyncDataNodeEcShardsAdvancesMaxVolumeId locks the same
// invariant on the incremental heartbeat path.
func TestIncrementalSyncDataNodeEcShardsAdvancesMaxVolumeId(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 100})
const ecVid = uint32(39849)
msg := buildEcShardMessage(ecVid, "prefeitura", "", 0, []erasure_coding.ShardId{0, 1, 2, 3})
topo.IncrementalSyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{msg}, nil, dn)
if got := topo.GetMaxVolumeId(); got != needle.VolumeId(ecVid) {
t.Fatalf("maxVolumeId after incremental EC registration = %d, want %d", got, ecVid)
}
}

View File

@@ -0,0 +1,135 @@
package topology
import (
"math/rand/v2"
"time"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"google.golang.org/grpc"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/storage"
)
func (t *Topology) StartRefreshWritableVolumes(grpcDialOption grpc.DialOption, garbageThreshold float64, concurrentVacuumLimitPerVolumeServer int, growThreshold float64, preallocate int64) {
go func() {
for {
if t.IsLeader() {
freshThreshHold := time.Now().Unix() - 3*t.pulse //3 times of sleep interval
t.CollectDeadNodeAndFullVolumes(freshThreshHold, t.volumeSizeLimit, growThreshold)
t.DecayQuietVolumeSizes()
}
time.Sleep(time.Duration(float32(t.pulse*1e3)*(1+rand.Float32())) * time.Millisecond)
}
}()
go func(garbageThreshold float64) {
for {
if t.IsLeader() {
// Safety net: if vacuum was disabled by the plugin monitor but the
// admin server is no longer connected, automatically re-enable.
// This handles the case where the admin server crashes without
// cleanup. Does NOT override an operator's intentional disable.
if t.IsVacuumDisabledByPlugin() && t.adminServerConnectedFunc != nil && !t.adminServerConnectedFunc() {
glog.V(0).Infof("Admin server disconnected while vacuum was disabled by plugin, clearing plugin disable")
t.EnableVacuumByPlugin()
}
if !t.IsVacuumDisabled() {
t.Vacuum(grpcDialOption, garbageThreshold, concurrentVacuumLimitPerVolumeServer, 0, "", preallocate, true)
}
} else {
stats.MasterReplicaPlacementMismatch.Reset()
}
time.Sleep(14*time.Minute + time.Duration(120*rand.Float32())*time.Second)
}
}(garbageThreshold)
go func() {
for {
select {
case fv := <-t.chanFullVolumes:
t.SetVolumeCapacityFull(fv)
case cv := <-t.chanCrowdedVolumes:
t.SetVolumeCrowded(cv)
}
}
}()
}
func (t *Topology) SetVolumeCapacityFull(volumeInfo storage.VolumeInfo) bool {
diskType := types.ToDiskType(volumeInfo.DiskType)
vl := t.GetVolumeLayout(volumeInfo.Collection, volumeInfo.ReplicaPlacement, volumeInfo.Ttl, diskType)
if !vl.SetVolumeCapacityFull(volumeInfo.Id) {
return false
}
vl.accessLock.RLock()
defer vl.accessLock.RUnlock()
vidLocations, found := vl.vid2location[volumeInfo.Id]
if !found {
return false
}
for _, dn := range vidLocations.list {
if !volumeInfo.ReadOnly {
disk := dn.getOrCreateDisk(volumeInfo.DiskType)
disk.UpAdjustDiskUsageDelta(types.ToDiskType(volumeInfo.DiskType), &DiskUsageCounts{
activeVolumeCount: -1,
})
}
}
return true
}
func (t *Topology) SetVolumeCrowded(volumeInfo storage.VolumeInfo) {
diskType := types.ToDiskType(volumeInfo.DiskType)
vl := t.GetVolumeLayout(volumeInfo.Collection, volumeInfo.ReplicaPlacement, volumeInfo.Ttl, diskType)
vl.SetVolumeCrowded(volumeInfo.Id)
}
// SetDataNodeMaintenanceMode records the maintenance flag a volume server
// reported and returns whether it changed. On a change every volume the node
// holds is re-evaluated: while the flag is on, a volume with a replica there
// leaves the writable list, so no writes are assigned to it and the server can
// be drained; once the flag is off the volumes are writable again. Reads are
// untouched, and volume growth skips the node through AvailableSpaceFor.
func (t *Topology) SetDataNodeMaintenanceMode(dn *DataNode, on bool) (changed bool) {
if !dn.SetMaintenanceMode(on) {
return false
}
glog.V(0).Infof("volume server %s maintenance mode: %v", dn.Id(), on)
for _, v := range dn.GetVolumes() {
diskType := types.ToDiskType(v.DiskType)
vl := t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
vl.EnsureCorrectWritables(&v)
}
return true
}
func (t *Topology) UnRegisterDataNode(dn *DataNode) {
dn.IsTerminating = true
for _, v := range dn.GetVolumes() {
glog.V(0).Infoln("Removing Volume", v.Id, "from the dead volume server", dn.Id())
diskType := types.ToDiskType(v.DiskType)
vl := t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
vl.SetVolumeUnavailable(dn, v.Id)
}
// unregister ec shards when volume server disconnected
for _, s := range dn.GetEcShards() {
t.UnRegisterEcShards(s, dn)
}
negativeUsages := dn.GetDiskUsages().negative()
for dt, du := range negativeUsages.usages {
dn.UpAdjustDiskUsageDelta(dt, du)
}
dn.DeltaUpdateVolumes([]storage.VolumeInfo{}, dn.GetVolumes())
dn.DeltaUpdateEcShards([]*erasure_coding.EcVolumeInfo{}, dn.GetEcShards())
t.unregisterDataNodeAddress(dn.ServerAddress(), dn)
if dn.Parent() != nil {
dn.Parent().UnlinkChildNode(dn.Id())
}
}

View File

@@ -0,0 +1,52 @@
package topology
import (
"fmt"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
)
func benchHeartbeatMessages(count int) []*master_pb.VolumeInformationMessage {
messages := make([]*master_pb.VolumeInformationMessage, 0, count)
for i := 0; i < count; i++ {
messages = append(messages, &master_pb.VolumeInformationMessage{
Id: uint32(i),
Size: 1024 * 1024,
Collection: "benchcollection",
FileCount: 100,
DeleteCount: 1,
DeletedByteCount: 1024,
ReplicaPlacement: 0,
Version: 3,
CompactRevision: 1,
ModifiedAtSecond: 1700000000,
})
}
return messages
}
// A volume server re-sends its whole volume list on every heartbeat, so this is
// the master's steady-state cost per volume server every VolumePulsePeriod.
func benchSyncDataNodeRegistration(b *testing.B, count int) {
topo := NewTopology("bench", nil, 32*1024*1024*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("127.0.0.1", 8080, 18080, "", "", map[string]uint32{"": uint32(count) * 2})
topo.SyncDataNodeRegistration(benchHeartbeatMessages(count), dn)
messages := benchHeartbeatMessages(count)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
topo.SyncDataNodeRegistration(messages, dn)
}
}
func BenchmarkSyncDataNodeRegistration(b *testing.B) {
for _, count := range []int{1000, 100000} {
b.Run(fmt.Sprintf("%dVolumes", count), func(b *testing.B) {
benchSyncDataNodeRegistration(b, count)
})
}
}

View File

@@ -0,0 +1,141 @@
package topology
import (
"slices"
"strings"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
)
type TopologyInfo struct {
Max int64 `json:"Max"`
Free int64 `json:"Free"`
DataCenters []DataCenterInfo `json:"DataCenters"`
Layouts []VolumeLayoutInfo `json:"Layouts"`
}
type VolumeLayoutCollection struct {
Collection string
VolumeLayout *VolumeLayout
}
func (t *Topology) ToInfo() (info TopologyInfo) {
info.Max = t.diskUsages.GetMaxVolumeCount()
info.Free = t.diskUsages.FreeSpace()
var dcs []DataCenterInfo
for _, c := range t.Children() {
dc := c.(*DataCenter)
dcs = append(dcs, dc.ToInfo())
}
slices.SortFunc(dcs, func(a, b DataCenterInfo) int {
return strings.Compare(string(a.Id), string(b.Id))
})
info.DataCenters = dcs
var layouts []VolumeLayoutInfo
for _, col := range t.collectionMap.Items() {
c := col.(*Collection)
for _, layout := range c.storageType2VolumeLayout.Items() {
if layout != nil {
tmp := layout.(*VolumeLayout).ToInfo()
tmp.Collection = c.Name
layouts = append(layouts, tmp)
}
}
}
info.Layouts = layouts
return
}
func (t *Topology) ListVolumeLayoutCollections() (volumeLayouts []*VolumeLayoutCollection) {
for _, col := range t.collectionMap.Items() {
for _, volumeLayout := range col.(*Collection).storageType2VolumeLayout.Items() {
volumeLayouts = append(volumeLayouts,
&VolumeLayoutCollection{col.(*Collection).Name, volumeLayout.(*VolumeLayout)},
)
}
}
return volumeLayouts
}
func (t *Topology) ToVolumeMap() interface{} {
m := make(map[string]interface{})
m["Max"] = t.diskUsages.GetMaxVolumeCount()
m["Free"] = t.diskUsages.FreeSpace()
dcs := make(map[NodeId]interface{})
for _, c := range t.Children() {
dc := c.(*DataCenter)
racks := make(map[NodeId]interface{})
for _, r := range dc.Children() {
rack := r.(*Rack)
dataNodes := make(map[NodeId]interface{})
for _, d := range rack.Children() {
dn := d.(*DataNode)
dnVolumes := dn.GetVolumes()
volumes := make([]interface{}, 0, len(dnVolumes))
for _, v := range dnVolumes {
volumes = append(volumes, v)
}
dataNodes[d.Id()] = volumes
}
racks[r.Id()] = dataNodes
}
dcs[dc.Id()] = racks
}
m["DataCenters"] = dcs
return m
}
// ToVolumeLocations snapshots every data node's volume set into a list of
// per-node VolumeLocation messages. NewVids carries every volume; RemoteVids
// repeats the remote-tier subset so clients can route reads to a local
// replica first when one exists on any node. EC shards are flattened into
// NewEcVids so each vid is reported once even when its shards live on
// multiple disks of the same node.
func (t *Topology) ToVolumeLocations() (volumeLocations []*master_pb.VolumeLocation) {
for _, c := range t.Children() {
dc := c.(*DataCenter)
for _, r := range dc.Children() {
rack := r.(*Rack)
for _, d := range rack.Children() {
dn := d.(*DataNode)
volumeLocation := &master_pb.VolumeLocation{
Url: dn.Url(),
PublicUrl: dn.PublicUrl,
DataCenter: dn.GetDataCenterId(),
GrpcPort: uint32(dn.GrpcPort),
}
volumeLocation.NewVids, volumeLocation.RemoteVids, volumeLocation.ReadOnlyVids, volumeLocation.ReadOnlyCanDeleteVids = dn.AppendVolumeIds(nil, nil, nil, nil)
// A single EC volume's shards can live on multiple disks of
// one DataNode, so GetEcShards returns per-(vid,disk) entries.
// Dedupe so the snapshot carries each vid once.
seenEcVids := make(map[uint32]struct{})
for _, s := range dn.GetEcShards() {
vid := uint32(s.VolumeId)
if _, ok := seenEcVids[vid]; ok {
continue
}
seenEcVids[vid] = struct{}{}
volumeLocation.NewEcVids = append(volumeLocation.NewEcVids, vid)
}
volumeLocations = append(volumeLocations, volumeLocation)
}
}
}
return
}
func (t *Topology) ToTopologyInfo(filter VolumeFilter) *master_pb.TopologyInfo {
m := &master_pb.TopologyInfo{
Id: string(t.Id()),
DiskInfos: t.diskUsages.ToDiskInfo(),
}
for _, c := range t.Children() {
dc := c.(*DataCenter)
m.DataCenterInfos = append(m.DataCenterInfos, dc.ToDataCenterInfo(filter))
}
return m
}

View File

@@ -0,0 +1,135 @@
package topology
import (
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
func TestCollectionVolumeStats(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
maxVolumeCounts := map[string]uint32{"": 25, "ssd": 12}
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", maxVolumeCounts)
volumeMessages := []*master_pb.VolumeInformationMessage{
{Id: 1, Size: 1000, Collection: "", FileCount: 10, ReplicaPlacement: 0, Version: uint32(needle.GetCurrentVersion())},
{Id: 2, Size: 2000, Collection: "c1", FileCount: 20, ReplicaPlacement: 0, Version: uint32(needle.GetCurrentVersion())},
{Id: 3, Size: 3000, Collection: "c1", FileCount: 30, ReplicaPlacement: 0, Version: uint32(needle.GetCurrentVersion()), DiskType: "ssd"},
{Id: 4, Size: 4000, Collection: "c2", FileCount: 40, ReplicaPlacement: 1, Version: uint32(needle.GetCurrentVersion())},
}
topo.SyncDataNodeRegistration(volumeMessages, dn)
// volume 4 is replicated, so a second node holds a copy of it
replicaDn := rack.GetOrCreateDataNode("127.0.0.2", 34534, 0, "127.0.0.2", "", maxVolumeCounts)
topo.SyncDataNodeRegistration(volumeMessages[3:], replicaDn)
// VolumeLocationList.Stats only counts nodes connected for over a minute
dn.LastSeen = time.Now().Unix() - 61
replicaDn.LastSeen = dn.LastSeen
allStats := topo.CollectionVolumeStats("")
assert(t, "all collections used size", int(allStats.UsedSize), 14000)
assert(t, "all collections logical used size", int(allStats.LogicalUsedSize), 10000)
assert(t, "all collections file count", int(allStats.FileCount), 100)
c1Stats := topo.CollectionVolumeStats("c1")
assert(t, "c1 used size across disk types", int(c1Stats.UsedSize), 5000)
assert(t, "c1 file count", int(c1Stats.FileCount), 50)
c2Stats := topo.CollectionVolumeStats("c2")
// both copies of volume 4 count against the space, only one against the data
assert(t, "c2 used size", int(c2Stats.UsedSize), 8000)
assert(t, "c2 logical used size", int(c2Stats.LogicalUsedSize), 4000)
missingStats := topo.CollectionVolumeStats("no-such-collection")
assert(t, "missing collection used size", int(missingStats.UsedSize), 0)
if _, found := topo.FindCollection("no-such-collection"); found {
t.Errorf("stats query should not create a phantom collection")
}
}
// ecShardMessage builds a heartbeat message for the given shard ids, sized
// (id+1)*sizeUnit bytes each.
func ecShardMessage(vid uint32, collection string, sizeUnit int, fileCount, deleteCount uint64, shardIds ...erasure_coding.ShardId) *master_pb.VolumeEcShardInformationMessage {
shards := erasure_coding.NewShardsInfo()
for _, id := range shardIds {
shards.Set(erasure_coding.NewShardInfo(id, erasure_coding.ShardSize((int(id)+1)*sizeUnit)))
}
return &master_pb.VolumeEcShardInformationMessage{
Id: vid,
Collection: collection,
EcIndexBits: shards.Bitmap(),
ShardSizes: shards.SizesInt64(),
FileCount: fileCount,
DeleteCount: deleteCount,
}
}
func TestCollectionVolumeStatsWithEcVolumes(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
maxVolumeCounts := map[string]uint32{"": 25}
dn1 := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", maxVolumeCounts)
dn2 := rack.GetOrCreateDataNode("127.0.0.2", 34534, 0, "127.0.0.2", "", maxVolumeCounts)
// volume 10 spans both nodes, and shard 0 has a second copy on dn2. dn2 has
// not finished loading its .ecx yet and reports a zero file count, and both
// nodes report the 5 tombstones of the journal they share.
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
ecShardMessage(10, "c1", 100, 50, 5, 0, 1, 2, 3, 4, 5, 6),
ecShardMessage(20, "", 10, 7, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13),
}, dn1)
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
ecShardMessage(10, "c1", 100, 0, 5, 0, 7, 8, 9, 10, 11, 12, 13),
}, dn2)
// every shard copy of volume 10, parity included, each sized (id+1)*100:
// 100..700 on dn1, plus a second copy of shard 0 and 800..1400 on dn2
c1Stats := topo.CollectionVolumeStats("c1")
assert(t, "c1 ec used size", int(c1Stats.UsedSize), 2800+7800)
// data shards 0..9 once each, so the second copy of shard 0 and the
// parity shards 10..13 are left out
assert(t, "c1 ec logical used size", int(c1Stats.LogicalUsedSize), 5500)
// the shared journal counts once, not once per holder: 50 - 5
assert(t, "c1 ec file count", int(c1Stats.FileCount), 45)
// volume 20 adds all 14 shards, each sized (id+1)*10
allStats := topo.CollectionVolumeStats("")
assert(t, "all collections ec used size", int(allStats.UsedSize), 10600+1050)
assert(t, "all collections ec logical used size", int(allStats.LogicalUsedSize), 5500+550)
assert(t, "all collections ec file count", int(allStats.FileCount), 45+7)
if _, found := topo.FindCollection("c1"); found {
t.Errorf("ec stats query should not create a phantom collection")
}
}
func TestCollectionVolumeStatsClampsDeletedOverTotals(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 25})
// Volume 1 reports more deleted than it holds, on both counters.
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
{Id: 1, Size: 1000, FileCount: 10, DeletedByteCount: 4000, DeleteCount: 40, Version: uint32(needle.GetCurrentVersion())},
{Id: 2, Size: 3000, FileCount: 30, DeletedByteCount: 1000, DeleteCount: 10, Version: uint32(needle.GetCurrentVersion())},
}, dn)
// VolumeLocationList.Stats only counts nodes connected for over a minute
dn.LastSeen = time.Now().Unix() - 61
stats := topo.CollectionVolumeStats("")
// Volume 1 contributes nothing rather than wrapping.
assert(t, "used size", int(stats.UsedSize), 2000)
assert(t, "logical used size", int(stats.LogicalUsedSize), 2000)
assert(t, "file count", int(stats.FileCount), 20)
}

View File

@@ -0,0 +1,866 @@
package topology
import (
"reflect"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
func TestRemoveDataCenter(t *testing.T) {
topo := setup(topologyLayout)
topo.UnlinkChildNode(NodeId("dc2"))
if topo.diskUsages.usages[types.HardDriveType].activeVolumeCount != 15 {
t.Fail()
}
topo.UnlinkChildNode(NodeId("dc3"))
if topo.diskUsages.usages[types.HardDriveType].activeVolumeCount != 12 {
t.Fail()
}
}
func TestHandlingVolumeServerHeartbeat(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
maxVolumeCounts := make(map[string]uint32)
maxVolumeCounts[""] = 25
maxVolumeCounts["ssd"] = 12
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", maxVolumeCounts)
{
volumeCount := 7
var volumeMessages []*master_pb.VolumeInformationMessage
for k := 1; k <= volumeCount; k++ {
volumeMessage := &master_pb.VolumeInformationMessage{
Id: uint32(k),
Size: uint64(25432),
Collection: "",
FileCount: uint64(2343),
DeleteCount: uint64(345),
DeletedByteCount: 34524,
ReadOnly: false,
ReplicaPlacement: uint32(0),
Version: uint32(needle.GetCurrentVersion()),
Ttl: 0,
}
volumeMessages = append(volumeMessages, volumeMessage)
}
for k := 1; k <= volumeCount; k++ {
volumeMessage := &master_pb.VolumeInformationMessage{
Id: uint32(volumeCount + k),
Size: uint64(25432),
Collection: "",
FileCount: uint64(2343),
DeleteCount: uint64(345),
DeletedByteCount: 34524,
ReadOnly: false,
ReplicaPlacement: uint32(0),
Version: uint32(needle.GetCurrentVersion()),
Ttl: 0,
DiskType: "ssd",
}
volumeMessages = append(volumeMessages, volumeMessage)
}
topo.SyncDataNodeRegistration(volumeMessages, dn)
usageCounts := topo.diskUsages.usages[types.HardDriveType]
assert(t, "activeVolumeCount1", int(usageCounts.activeVolumeCount), volumeCount)
assert(t, "volumeCount", int(usageCounts.volumeCount), volumeCount)
assert(t, "ssdVolumeCount", int(topo.diskUsages.usages[types.SsdType].volumeCount), volumeCount)
}
{
volumeCount := 7 - 1
var volumeMessages []*master_pb.VolumeInformationMessage
for k := 1; k <= volumeCount; k++ {
volumeMessage := &master_pb.VolumeInformationMessage{
Id: uint32(k),
Size: uint64(30000),
Collection: "",
FileCount: uint64(2343),
DeleteCount: uint64(345),
DeletedByteCount: 345240,
ReadOnly: false,
ReplicaPlacement: uint32(0),
Version: uint32(needle.GetCurrentVersion()),
Ttl: 0,
}
volumeMessages = append(volumeMessages, volumeMessage)
}
topo.SyncDataNodeRegistration(volumeMessages, dn)
//rp, _ := storage.NewReplicaPlacementFromString("000")
//layout := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL)
//assert(t, "writables", len(layout.writables), volumeCount)
usageCounts := topo.diskUsages.usages[types.HardDriveType]
assert(t, "activeVolumeCount1", int(usageCounts.activeVolumeCount), volumeCount)
assert(t, "volumeCount", int(usageCounts.volumeCount), volumeCount)
}
{
volumeCount := 6
newVolumeShortMessage := &master_pb.VolumeShortInformationMessage{
Id: uint32(3),
Collection: "",
ReplicaPlacement: uint32(0),
Version: uint32(needle.GetCurrentVersion()),
Ttl: 0,
}
topo.IncrementalSyncDataNodeRegistration(
[]*master_pb.VolumeShortInformationMessage{newVolumeShortMessage},
nil,
dn)
rp, _ := super_block.NewReplicaPlacementFromString("000")
layout := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
assert(t, "writables after repeated add", len(layout.writables), volumeCount)
usageCounts := topo.diskUsages.usages[types.HardDriveType]
assert(t, "activeVolumeCount1", int(usageCounts.activeVolumeCount), volumeCount)
assert(t, "volumeCount", int(usageCounts.volumeCount), volumeCount)
topo.IncrementalSyncDataNodeRegistration(
nil,
[]*master_pb.VolumeShortInformationMessage{newVolumeShortMessage},
dn)
assert(t, "writables after deletion", len(layout.writables), volumeCount-1)
assert(t, "activeVolumeCount1", int(usageCounts.activeVolumeCount), volumeCount-1)
assert(t, "volumeCount", int(usageCounts.volumeCount), volumeCount-1)
topo.IncrementalSyncDataNodeRegistration(
[]*master_pb.VolumeShortInformationMessage{newVolumeShortMessage},
nil,
dn)
for vid := range layout.vid2location {
println("after add volume id", vid)
}
for _, vid := range layout.writables {
println("after add writable volume id", vid)
}
assert(t, "writables after add back", len(layout.writables), volumeCount)
}
topo.UnRegisterDataNode(dn)
usageCounts := topo.diskUsages.usages[types.HardDriveType]
assert(t, "activeVolumeCount2", int(usageCounts.activeVolumeCount), 0)
}
func TestIncrementalSyncReplacesVolumeReadOnlyState(t *testing.T) {
for _, tc := range []struct {
name string
fromReadOnly bool
toReadOnly bool
}{
{name: "writable to read-only", toReadOnly: true},
{name: "read-only to writable", fromReadOnly: true},
} {
t.Run(tc.name, func(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 25})
volume := func(readOnly bool) *master_pb.VolumeShortInformationMessage {
return &master_pb.VolumeShortInformationMessage{
Id: 1, Collection: "c", Version: uint32(needle.GetCurrentVersion()), ReadOnly: readOnly,
}
}
oldVolume := volume(tc.fromReadOnly)
topo.IncrementalSyncDataNodeRegistration([]*master_pb.VolumeShortInformationMessage{oldVolume}, nil, dn)
topo.IncrementalSyncDataNodeRegistration(
[]*master_pb.VolumeShortInformationMessage{volume(tc.toReadOnly)},
[]*master_pb.VolumeShortInformationMessage{oldVolume}, dn)
locations := topo.Lookup("c", needle.VolumeId(1))
if len(locations) != 1 || locations[0] != dn {
t.Fatalf("lookup locations = %v, want only %v", locations, dn)
}
stored, err := dn.GetVolumesById(needle.VolumeId(1))
if err != nil || stored.ReadOnly != tc.toReadOnly {
t.Fatalf("stored volume = %+v, err = %v, want read-only %t", stored, err, tc.toReadOnly)
}
rp, _ := super_block.NewReplicaPlacementFromString("000")
active, _ := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType).GetWritableVolumeCount()
want := 0
if !tc.toReadOnly {
want = 1
}
if active != want {
t.Fatalf("writable count = %d, want %d", active, want)
}
if !dn.HasConsistentVolumeIndex() {
t.Fatal("replacement left the held and servable volume indexes inconsistent")
}
})
}
}
func TestIncrementalSyncRegistersMovedVolumeBeforeRemoval(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 25, "ssd": 25})
oldVolume := &master_pb.VolumeShortInformationMessage{
Id: 1, Collection: "c", Version: uint32(needle.GetCurrentVersion()),
}
newVolume := &master_pb.VolumeShortInformationMessage{
Id: 1, Collection: "c", Version: uint32(needle.GetCurrentVersion()), DiskType: "ssd",
}
topo.IncrementalSyncDataNodeRegistration([]*master_pb.VolumeShortInformationMessage{oldVolume}, nil, dn)
rp, _ := super_block.NewReplicaPlacementFromString("000")
oldLayout := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)
newLayout := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.SsdType)
oldLayout.accessLock.Lock()
done := make(chan struct{})
go func() {
topo.IncrementalSyncDataNodeRegistration(
[]*master_pb.VolumeShortInformationMessage{newVolume},
[]*master_pb.VolumeShortInformationMessage{oldVolume}, dn)
close(done)
}()
deadline := time.NewTimer(time.Second)
defer deadline.Stop()
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
movedBeforeRemoval := false
for !movedBeforeRemoval {
select {
case <-deadline.C:
oldLayout.accessLock.Unlock()
<-done
t.Fatal("destination layout was not registered before source removal")
case <-ticker.C:
movedBeforeRemoval = len(newLayout.Lookup(needle.VolumeId(1))) == 1
}
}
oldLayout.accessLock.Unlock()
<-done
locations := topo.Lookup("c", needle.VolumeId(1))
if len(locations) != 1 || locations[0] != dn {
t.Fatalf("lookup locations = %v, want only %v", locations, dn)
}
}
func TestDataNodeToDataNodeInfo_IncludeEmptyDiskFromUsage(t *testing.T) {
dn := NewDataNode("node-1")
dn.Ip = "127.0.0.1"
dn.Port = 18080
dn.GrpcPort = 28080
// Simulate a node that has slot counters but no mounted volumes yet.
usage := dn.diskUsages.getOrCreateDisk(types.HardDriveType)
usage.maxVolumeCount = 8
info := dn.ToDataNodeInfo(VolumeFilter{})
diskInfo, found := info.DiskInfos[""]
if !found {
t.Fatalf("expected default disk entry for empty node")
}
if diskInfo.MaxVolumeCount != 8 {
t.Fatalf("unexpected max volume count: got=%d want=8", diskInfo.MaxVolumeCount)
}
if len(diskInfo.VolumeInfos) != 0 {
t.Fatalf("expected no volumes for empty disk, got=%d", len(diskInfo.VolumeInfos))
}
}
func assert(t *testing.T, message string, actual, expected int) {
if actual != expected {
t.Fatalf("unexpected %s: %d, expected: %d", message, actual, expected)
}
}
func TestAddRemoveVolume(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
maxVolumeCounts := make(map[string]uint32)
maxVolumeCounts[""] = 25
maxVolumeCounts["ssd"] = 12
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", maxVolumeCounts)
v := storage.VolumeInfo{
Id: needle.VolumeId(1),
Size: 100,
Collection: "xcollection",
DiskType: "ssd",
FileCount: 123,
DeleteCount: 23,
DeletedByteCount: 45,
ReadOnly: false,
Version: needle.GetCurrentVersion(),
ReplicaPlacement: &super_block.ReplicaPlacement{},
Ttl: needle.EMPTY_TTL,
}
dn.UpdateVolumes([]storage.VolumeInfo{v})
topo.RegisterVolumeLayout(v, dn)
topo.RegisterVolumeLayout(v, dn)
if _, hasCollection := topo.FindCollection(v.Collection); !hasCollection {
t.Errorf("collection %v should exist", v.Collection)
}
topo.UnRegisterVolumeLayout(v, dn)
if _, hasCollection := topo.FindCollection(v.Collection); hasCollection {
t.Errorf("collection %v should not exist", v.Collection)
}
}
func TestUnRegisterVolumeLayoutClearsReplicaPlacementMismatchMetric(t *testing.T) {
stats.MasterReplicaPlacementMismatch.Reset()
t.Cleanup(stats.MasterReplicaPlacementMismatch.Reset)
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
maxVolumeCounts := map[string]uint32{"": 25}
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", maxVolumeCounts)
rp, err := super_block.NewReplicaPlacementFromString("001")
if err != nil {
t.Fatalf("NewReplicaPlacementFromString: %v", err)
}
v := storage.VolumeInfo{
Id: needle.VolumeId(42),
Size: 100,
Collection: "metrics-test",
ReplicaPlacement: rp,
Ttl: needle.EMPTY_TTL,
}
dn.UpdateVolumes([]storage.VolumeInfo{v})
topo.RegisterVolumeLayout(v, dn)
stats.MasterReplicaPlacementMismatch.WithLabelValues(v.Collection, v.Id.String()).Set(1)
if n := testutil.CollectAndCount(stats.MasterReplicaPlacementMismatch); n != 1 {
t.Fatalf("expected 1 replica_placement_mismatch series, got %d", n)
}
topo.UnRegisterVolumeLayout(v, dn)
if n := testutil.CollectAndCount(stats.MasterReplicaPlacementMismatch); n != 0 {
t.Errorf("%d replica_placement_mismatch series left after volume left topology", n)
}
}
func TestUnRegisterVolumeLayoutKeepsReplicaPlacementMismatchMetricWhilePlacementsRemain(t *testing.T) {
stats.MasterReplicaPlacementMismatch.Reset()
t.Cleanup(stats.MasterReplicaPlacementMismatch.Reset)
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
maxVolumeCounts := map[string]uint32{"": 25}
dn1 := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", maxVolumeCounts)
dn2 := rack.GetOrCreateDataNode("127.0.0.1", 34535, 0, "127.0.0.1", "", maxVolumeCounts)
rp, err := super_block.NewReplicaPlacementFromString("001")
if err != nil {
t.Fatalf("NewReplicaPlacementFromString: %v", err)
}
v := storage.VolumeInfo{
Id: needle.VolumeId(42),
Size: 100,
Collection: "metrics-test",
ReplicaPlacement: rp,
Ttl: needle.EMPTY_TTL,
}
dn1.UpdateVolumes([]storage.VolumeInfo{v})
dn2.UpdateVolumes([]storage.VolumeInfo{v})
topo.RegisterVolumeLayout(v, dn1)
topo.RegisterVolumeLayout(v, dn2)
stats.MasterReplicaPlacementMismatch.WithLabelValues(v.Collection, v.Id.String()).Set(1)
if n := testutil.CollectAndCount(stats.MasterReplicaPlacementMismatch); n != 1 {
t.Fatalf("expected 1 replica_placement_mismatch series, got %d", n)
}
topo.UnRegisterVolumeLayout(v, dn1)
if n := testutil.CollectAndCount(stats.MasterReplicaPlacementMismatch); n != 1 {
t.Errorf("expected series to remain while %s still holds the volume, got %d", dn2.Id(), n)
}
if got := len(topo.Lookup(v.Collection, v.Id)); got != 1 {
t.Fatalf("expected 1 remaining placement, got %d", got)
}
topo.UnRegisterVolumeLayout(v, dn2)
if n := testutil.CollectAndCount(stats.MasterReplicaPlacementMismatch); n != 0 {
t.Errorf("%d replica_placement_mismatch series left after last placement left", n)
}
}
func TestVolumeReadOnlyStatusChange(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
maxVolumeCounts := make(map[string]uint32)
maxVolumeCounts[""] = 25
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", maxVolumeCounts)
// Create a writable volume
v := storage.VolumeInfo{
Id: needle.VolumeId(1),
Size: 100,
Collection: "",
DiskType: "",
FileCount: 10,
DeleteCount: 0,
DeletedByteCount: 0,
ReadOnly: false, // Initially writable
Version: needle.GetCurrentVersion(),
ReplicaPlacement: &super_block.ReplicaPlacement{},
Ttl: needle.EMPTY_TTL,
}
dn.UpdateVolumes([]storage.VolumeInfo{v})
topo.RegisterVolumeLayout(v, dn)
// Check initial active count (should be 1 since volume is writable)
usageCounts := topo.diskUsages.usages[types.HardDriveType]
assert(t, "initial activeVolumeCount", int(usageCounts.activeVolumeCount), 1)
assert(t, "initial remoteVolumeCount", int(usageCounts.remoteVolumeCount), 0)
// Change volume to read-only
v.ReadOnly = true
dn.UpdateVolumes([]storage.VolumeInfo{v})
// Check active count after marking read-only (should be 0)
usageCounts = topo.diskUsages.usages[types.HardDriveType]
assert(t, "activeVolumeCount after read-only", int(usageCounts.activeVolumeCount), 0)
// Change volume back to writable
v.ReadOnly = false
dn.UpdateVolumes([]storage.VolumeInfo{v})
// Check active count after marking writable again (should be 1)
usageCounts = topo.diskUsages.usages[types.HardDriveType]
assert(t, "activeVolumeCount after writable again", int(usageCounts.activeVolumeCount), 1)
}
func TestVolumeReadOnlyAndRemoteStatusChange(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
maxVolumeCounts := make(map[string]uint32)
maxVolumeCounts[""] = 25
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", maxVolumeCounts)
// Create a writable, local volume
v := storage.VolumeInfo{
Id: needle.VolumeId(1),
Size: 100,
Collection: "",
DiskType: "",
FileCount: 10,
DeleteCount: 0,
DeletedByteCount: 0,
ReadOnly: false, // Initially writable
RemoteStorageName: "", // Initially local
Version: needle.GetCurrentVersion(),
ReplicaPlacement: &super_block.ReplicaPlacement{},
Ttl: needle.EMPTY_TTL,
}
dn.UpdateVolumes([]storage.VolumeInfo{v})
topo.RegisterVolumeLayout(v, dn)
// Check initial counts
usageCounts := topo.diskUsages.usages[types.HardDriveType]
assert(t, "initial activeVolumeCount", int(usageCounts.activeVolumeCount), 1)
assert(t, "initial remoteVolumeCount", int(usageCounts.remoteVolumeCount), 0)
// Simultaneously change to read-only AND remote
v.ReadOnly = true
v.RemoteStorageName = "s3"
v.RemoteStorageName = "s3.default"
dn.UpdateVolumes([]storage.VolumeInfo{v})
// Check counts after both changes
usageCounts = topo.diskUsages.usages[types.HardDriveType]
assert(t, "activeVolumeCount after read-only+remote", int(usageCounts.activeVolumeCount), 0)
assert(t, "remoteVolumeCount after read-only+remote", int(usageCounts.remoteVolumeCount), 1)
// Change back to writable but keep remote
v.ReadOnly = false
dn.UpdateVolumes([]storage.VolumeInfo{v})
// Check counts - should be writable (active=1) and still remote
usageCounts = topo.diskUsages.usages[types.HardDriveType]
assert(t, "activeVolumeCount after writable+remote", int(usageCounts.activeVolumeCount), 1)
assert(t, "remoteVolumeCount after writable+remote", int(usageCounts.remoteVolumeCount), 1)
// Change back to local AND read-only simultaneously
v.ReadOnly = true
v.RemoteStorageName = ""
v.RemoteStorageName = ""
dn.UpdateVolumes([]storage.VolumeInfo{v})
// Check final counts
usageCounts = topo.diskUsages.usages[types.HardDriveType]
assert(t, "final activeVolumeCount", int(usageCounts.activeVolumeCount), 0)
assert(t, "final remoteVolumeCount", int(usageCounts.remoteVolumeCount), 0)
}
func TestListCollections(t *testing.T) {
rp, _ := super_block.NewReplicaPlacementFromString("002")
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", nil)
topo.RegisterVolumeLayout(storage.VolumeInfo{
Id: needle.VolumeId(1111),
ReplicaPlacement: rp,
}, dn)
topo.RegisterVolumeLayout(storage.VolumeInfo{
Id: needle.VolumeId(2222),
ReplicaPlacement: rp,
Collection: "vol_collection_a",
}, dn)
topo.RegisterVolumeLayout(storage.VolumeInfo{
Id: needle.VolumeId(3333),
ReplicaPlacement: rp,
Collection: "vol_collection_b",
}, dn)
topo.RegisterEcShards(&erasure_coding.EcVolumeInfo{
VolumeId: needle.VolumeId(4444),
Collection: "ec_collection_a",
ShardsInfo: erasure_coding.NewShardsInfo(),
}, dn)
topo.RegisterEcShards(&erasure_coding.EcVolumeInfo{
VolumeId: needle.VolumeId(5555),
Collection: "ec_collection_b",
ShardsInfo: erasure_coding.NewShardsInfo(),
}, dn)
testCases := []struct {
name string
includeNormalVolumes bool
includeEcVolumes bool
want []string
}{
{
name: "no volume types selected",
includeNormalVolumes: false,
includeEcVolumes: false,
want: nil,
}, {
name: "normal volumes",
includeNormalVolumes: true,
includeEcVolumes: false,
want: []string{"", "vol_collection_a", "vol_collection_b"},
}, {
name: "EC volumes",
includeNormalVolumes: false,
includeEcVolumes: true,
want: []string{"ec_collection_a", "ec_collection_b"},
}, {
name: "normal + EC volumes",
includeNormalVolumes: true,
includeEcVolumes: true,
want: []string{"", "ec_collection_a", "ec_collection_b", "vol_collection_a", "vol_collection_b"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := topo.ListCollections(tc.includeNormalVolumes, tc.includeEcVolumes)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("got %v, want %v", got, tc.want)
}
})
}
}
func TestDataNodeIdBasedIdentification(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
maxVolumeCounts := make(map[string]uint32)
maxVolumeCounts[""] = 10
// Test 1: Create a DataNode with explicit id
dn1 := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "10.0.0.1:8080", "node-1", maxVolumeCounts)
if string(dn1.Id()) != "node-1" {
t.Errorf("expected node id 'node-1', got '%s'", dn1.Id())
}
if dn1.Ip != "10.0.0.1" {
t.Errorf("expected ip '10.0.0.1', got '%s'", dn1.Ip)
}
// Test 2: Same id with different IP should return the same DataNode (K8s pod reschedule scenario)
dn2 := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "10.0.0.2:8080", "node-1", maxVolumeCounts)
if dn1 != dn2 {
t.Errorf("expected same DataNode for same id, got different nodes")
}
// IP should be updated to the new value
if dn2.Ip != "10.0.0.2" {
t.Errorf("expected ip to be updated to '10.0.0.2', got '%s'", dn2.Ip)
}
if dn2.PublicUrl != "10.0.0.2:8080" {
t.Errorf("expected publicUrl to be updated to '10.0.0.2:8080', got '%s'", dn2.PublicUrl)
}
// Test 3: Different id should create a new DataNode
dn3 := rack.GetOrCreateDataNode("10.0.0.3", 8080, 18080, "10.0.0.3:8080", "node-2", maxVolumeCounts)
if string(dn3.Id()) != "node-2" {
t.Errorf("expected node id 'node-2', got '%s'", dn3.Id())
}
if dn1 == dn3 {
t.Errorf("expected different DataNode for different id")
}
// Test 4: Empty id should fall back to ip:port (backward compatibility)
dn4 := rack.GetOrCreateDataNode("10.0.0.4", 8080, 18080, "10.0.0.4:8080", "", maxVolumeCounts)
if string(dn4.Id()) != "10.0.0.4:8080" {
t.Errorf("expected node id '10.0.0.4:8080' for empty id, got '%s'", dn4.Id())
}
// Test 5: Same ip:port with empty id should return the same DataNode
dn5 := rack.GetOrCreateDataNode("10.0.0.4", 8080, 18080, "10.0.0.4:8080", "", maxVolumeCounts)
if dn4 != dn5 {
t.Errorf("expected same DataNode for same ip:port with empty id")
}
// Verify we have 3 unique DataNodes total:
// - node-1 (dn1/dn2 share the same id)
// - node-2 (dn3)
// - 10.0.0.4:8080 (dn4/dn5 share the same ip:port)
children := rack.Children()
if len(children) != 3 {
t.Errorf("expected 3 DataNodes, got %d", len(children))
}
// Test 6: Transition from ip:port to explicit id
// First, the node exists with ip:port as id (dn4/dn5)
// Now the same volume server starts sending an explicit id
dn6 := rack.GetOrCreateDataNode("10.0.0.4", 8080, 18080, "10.0.0.4:8080", "node-4-explicit", maxVolumeCounts)
// Should return the same DataNode instance
if dn6 != dn4 {
t.Errorf("expected same DataNode instance during transition")
}
// But the id should now be updated to the explicit id
if string(dn6.Id()) != "node-4-explicit" {
t.Errorf("expected node id to transition to 'node-4-explicit', got '%s'", dn6.Id())
}
// The node should be re-keyed in the children map
if rack.FindDataNodeById("node-4-explicit") != dn6 {
t.Errorf("expected to find DataNode by new explicit id")
}
// Old ip:port key should no longer work
if rack.FindDataNodeById("10.0.0.4:8080") != nil {
t.Errorf("expected old ip:port id to be removed from children map")
}
// Still 3 unique DataNodes (node-1, node-2, node-4-explicit)
children = rack.Children()
if len(children) != 3 {
t.Errorf("expected 3 DataNodes after transition, got %d", len(children))
}
// Test 7: Prevent incorrect transition when a new node reuses ip:port of a node with explicit id
// Scenario: node-1 runs at 10.0.0.1:8080, dies, new node-99 starts at same ip:port
// The transition should NOT happen because node-1 already has an explicit id
dn7 := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "10.0.0.1:8080", "node-99", maxVolumeCounts)
// Should create a NEW DataNode, not reuse node-1
if dn7 == dn1 {
t.Errorf("expected new DataNode for node-99, got reused node-1")
}
if string(dn7.Id()) != "node-99" {
t.Errorf("expected node id 'node-99', got '%s'", dn7.Id())
}
// node-1 should still exist with its original id
if rack.FindDataNodeById("node-1") == nil {
t.Errorf("node-1 should still exist")
}
// Now we have 4 DataNodes
children = rack.Children()
if len(children) != 4 {
t.Errorf("expected 4 DataNodes, got %d", len(children))
}
}
func TestLookupDataNodeByAddress(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
maxVolumeCounts := map[string]uint32{"": 10}
// Brand-new registration must be discoverable by both the http and
// grpc forms of the address.
dn := rack.GetOrCreateDataNode("10.1.2.3", 8080, 18080, "10.1.2.3:8080", "n1", maxVolumeCounts)
if got := topo.LookupDataNodeByAddress(pb.ServerAddress("10.1.2.3:8080")); got != dn {
t.Fatalf("lookup by http address: got %v, want %v", got, dn)
}
if got := topo.LookupDataNodeByAddress(pb.ServerAddress("10.1.2.3:8080.18080")); got != dn {
t.Fatalf("lookup by grpc-suffix address: got %v, want %v", got, dn)
}
// Unknown addresses must miss.
if got := topo.LookupDataNodeByAddress(pb.ServerAddress("127.0.0.1:1")); got != nil {
t.Fatalf("unknown address must not be found, got %v", got)
}
// Heartbeat from a moved pod (same id, new ip) updates the index in
// place: the old address is dropped and the new one resolves.
dnMoved := rack.GetOrCreateDataNode("10.9.9.9", 8080, 18080, "10.9.9.9:8080", "n1", maxVolumeCounts)
if dnMoved != dn {
t.Fatalf("expected same node instance after move, got different")
}
if got := topo.LookupDataNodeByAddress(pb.ServerAddress("10.1.2.3:8080")); got != nil {
t.Fatalf("old address must be unregistered after move, got %v", got)
}
if got := topo.LookupDataNodeByAddress(pb.ServerAddress("10.9.9.9:8080")); got != dn {
t.Fatalf("new address lookup: got %v, want %v", got, dn)
}
// UnRegisterDataNode evicts the index entry.
topo.UnRegisterDataNode(dn)
if got := topo.LookupDataNodeByAddress(pb.ServerAddress("10.9.9.9:8080")); got != nil {
t.Fatalf("address must be unregistered after UnRegisterDataNode, got %v", got)
}
}
// TestSyncDataNodeRegistrationReRegistersMissingVolume reproduces the divergence
// where a volume present on a data node (shown by volume.list / admin UI) is
// missing from the lookup index, which surfaces as "volume id not found" on
// LookupVolume. SetVolumeUnavailable (used by
// UnRegisterDataNode on a disconnect) drops the volume from the index, and the
// reconnecting full heartbeat used to skip it because it was no longer "new" to
// the disk map. The full heartbeat now self-heals.
func TestSyncDataNodeRegistrationReRegistersMissingVolume(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 25})
vid := needle.VolumeId(18994)
volumeMessage := &master_pb.VolumeInformationMessage{
Id: uint32(vid),
Size: 100,
Collection: "drr",
ReplicaPlacement: uint32(0),
Version: uint32(needle.GetCurrentVersion()),
Ttl: 0,
}
// Initial full heartbeat registers the volume in the lookup index.
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{volumeMessage}, dn)
if got := topo.Lookup("", vid); len(got) != 1 {
t.Fatalf("after registration: lookup %d got %v, want 1 location", vid, got)
}
// Drop the volume from the index the way UnRegisterDataNode does, but leave
// it in the data node's disk map (the reconnecting heartbeat did not report
// it as new).
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("drr", rp, needle.EMPTY_TTL, types.HardDriveType)
vl.SetVolumeUnavailable(dn, vid)
// The empty entry must be removed, otherwise Lookup returns a non-nil empty
// list that still reads as "not found".
if got := topo.Lookup("", vid); got != nil {
t.Fatalf("after SetVolumeUnavailable: expected lookup miss, got %v", got)
}
if _, err := dn.GetVolumesById(vid); err != nil {
t.Fatalf("volume %d should still be in the data node disk map: %v", vid, err)
}
// The next full heartbeat re-registers the volume even though it is not new.
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{volumeMessage}, dn)
if got := topo.Lookup("", vid); len(got) != 1 {
t.Fatalf("after self-heal: lookup %d got %v, want 1 location", vid, got)
}
}
// TestSetVolumeAvailableRepairsMissingVolume covers the vacuum-commit variant of
// the same divergence. A disconnect during a long vacuum can drop a
// single-replica volume from the lookup index while it stays on the node; the
// commit then calls SetVolumeAvailable on it. That used to dereference a nil
// location and panic; it now re-creates the entry and repairs the split.
func TestSetVolumeAvailableRepairsMissingVolume(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 25})
vid := needle.VolumeId(2640)
volumeMessage := &master_pb.VolumeInformationMessage{
Id: uint32(vid),
Size: 100,
Collection: "drr",
ReplicaPlacement: uint32(0),
Version: uint32(needle.GetCurrentVersion()),
Ttl: 0,
}
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{volumeMessage}, dn)
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("drr", rp, needle.EMPTY_TTL, types.HardDriveType)
// Disconnect drops the volume from the index but leaves it on the node.
vl.SetVolumeUnavailable(dn, vid)
if got := topo.Lookup("", vid); got != nil {
t.Fatalf("after SetVolumeUnavailable: expected lookup miss, got %v", got)
}
if _, err := dn.GetVolumesById(vid); err != nil {
t.Fatalf("volume %d should still be in the data node disk map: %v", vid, err)
}
// The vacuum commit re-marks the volume available; it must re-register it.
vl.SetVolumeAvailable(dn, vid, false, false)
if got := topo.Lookup("", vid); len(got) != 1 {
t.Fatalf("after SetVolumeAvailable: lookup %d got %v, want 1 location", vid, got)
}
// Size tracking must be seeded too, or assigns go uncounted until the next
// heartbeat and the volume can overfill.
vl.accessLock.RLock()
_, tracked := vl.sizeTracking[vid]
vl.accessLock.RUnlock()
if !tracked {
t.Fatalf("after SetVolumeAvailable: size tracking for %d not seeded", vid)
}
}

View File

@@ -0,0 +1,371 @@
package topology
import (
"context"
"io"
"sync"
"sync/atomic"
"time"
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/pb"
"google.golang.org/grpc"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/operation"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
func (t *Topology) batchVacuumVolumeCheck(grpcDialOption grpc.DialOption, vid needle.VolumeId,
locationlist *VolumeLocationList, garbageThreshold float64) (*VolumeLocationList, bool) {
ch := make(chan int, locationlist.Length())
errCount := int32(0)
for index, dn := range locationlist.list {
go func(index int, url pb.ServerAddress, vid needle.VolumeId) {
err := operation.WithVolumeServerClient(false, url, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
resp, err := volumeServerClient.VacuumVolumeCheck(context.Background(), &volume_server_pb.VacuumVolumeCheckRequest{
VolumeId: uint32(vid),
})
if err != nil {
atomic.AddInt32(&errCount, 1)
ch <- -1
return err
}
if resp.GarbageRatio >= garbageThreshold {
ch <- index
} else {
ch <- -1
}
return nil
})
if err != nil {
glog.V(0).Infof("Checking vacuuming %d on %s: %v", vid, url, err)
}
}(index, dn.ServerAddress(), vid)
}
vacuumLocationList := NewVolumeLocationList()
waitTimeout := time.NewTimer(time.Minute * time.Duration(t.volumeSizeLimit/1024/1024/1000+1))
defer waitTimeout.Stop()
for range locationlist.list {
select {
case index := <-ch:
if index != -1 {
vacuumLocationList.list = append(vacuumLocationList.list, locationlist.list[index])
}
case <-waitTimeout.C:
return vacuumLocationList, false
}
}
return vacuumLocationList, errCount == 0 && len(vacuumLocationList.list) > 0
}
func (t *Topology) batchVacuumVolumeCompact(grpcDialOption grpc.DialOption, vl *VolumeLayout, vid needle.VolumeId,
locationlist *VolumeLocationList, preallocate int64) bool {
vl.DrainAndRemoveFromWritable(vid)
ch := make(chan bool, locationlist.Length())
for index, dn := range locationlist.list {
go func(index int, url pb.ServerAddress, vid needle.VolumeId) {
glog.V(0).Infoln(index, "Start vacuuming", vid, "on", url)
err := operation.WithVolumeServerClient(true, url, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
stream, err := volumeServerClient.VacuumVolumeCompact(context.Background(), &volume_server_pb.VacuumVolumeCompactRequest{
VolumeId: uint32(vid),
Preallocate: preallocate,
})
if err != nil {
return err
}
for {
resp, recvErr := stream.Recv()
if recvErr != nil {
if recvErr == io.EOF {
break
} else {
return recvErr
}
}
glog.V(0).Infof("%d vacuum %d on %s processed %d bytes, loadAvg %.02f%%",
index, vid, url, resp.ProcessedBytes, resp.LoadAvg_1M*100)
}
return nil
})
if err != nil {
glog.Errorf("Error when vacuuming %d on %s: %v", vid, url, err)
ch <- false
} else {
glog.V(0).Infof("Complete vacuuming %d on %s", vid, url)
ch <- true
}
}(index, dn.ServerAddress(), vid)
}
isVacuumSuccess := true
waitTimeout := time.NewTimer(3 * time.Minute * time.Duration(t.volumeSizeLimit/1024/1024/1000+1))
defer waitTimeout.Stop()
for range locationlist.list {
select {
case canCommit := <-ch:
isVacuumSuccess = isVacuumSuccess && canCommit
case <-waitTimeout.C:
return false
}
}
return isVacuumSuccess
}
func (t *Topology) batchVacuumVolumeCommit(grpcDialOption grpc.DialOption, vl *VolumeLayout, vid needle.VolumeId, vacuumLocationList, locationList *VolumeLocationList) bool {
isCommitSuccess := true
isReadOnly := false
isFullCapacity := false
for _, dn := range vacuumLocationList.list {
glog.V(0).Infoln("Start Committing vacuum", vid, "on", dn.Url())
err := operation.WithVolumeServerClient(false, dn.ServerAddress(), grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
resp, err := volumeServerClient.VacuumVolumeCommit(context.Background(), &volume_server_pb.VacuumVolumeCommitRequest{
VolumeId: uint32(vid),
})
if resp != nil {
if resp.IsReadOnly {
isReadOnly = true
}
if resp.VolumeSize > t.volumeSizeLimit {
isFullCapacity = true
}
}
return err
})
if err != nil {
glog.Errorf("Error when committing vacuum %d on %s: %v", vid, dn.Url(), err)
isCommitSuccess = false
} else {
glog.V(0).Infof("Complete Committing vacuum %d on %s", vid, dn.Url())
}
}
//we should check the status of all replicas
if len(locationList.list) > len(vacuumLocationList.list) {
for _, dn := range locationList.list {
isFound := false
for _, dnVaccum := range vacuumLocationList.list {
if dn.id == dnVaccum.id {
isFound = true
break
}
}
if !isFound {
err := operation.WithVolumeServerClient(false, dn.ServerAddress(), grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
resp, err := volumeServerClient.VolumeStatus(context.Background(), &volume_server_pb.VolumeStatusRequest{
VolumeId: uint32(vid),
})
if resp != nil {
if resp.IsReadOnly {
isReadOnly = true
}
if resp.VolumeSize > t.volumeSizeLimit {
isFullCapacity = true
}
}
return err
})
if err != nil {
glog.Errorf("Error when checking volume %d status on %s: %v", vid, dn.Url(), err)
//we mark volume read-only, since the volume state is unknown
isReadOnly = true
}
}
}
}
if isCommitSuccess {
//record vacuum time of volume
vl.accessLock.Lock()
vl.vacuumedVolumes[vid] = time.Now()
vl.accessLock.Unlock()
for _, dn := range vacuumLocationList.list {
vl.SetVolumeAvailable(dn, vid, isReadOnly, isFullCapacity)
}
}
return isCommitSuccess
}
func (t *Topology) batchVacuumVolumeCleanup(grpcDialOption grpc.DialOption, vl *VolumeLayout, vid needle.VolumeId, locationlist *VolumeLocationList) {
for _, dn := range locationlist.list {
glog.V(0).Infoln("Start cleaning up", vid, "on", dn.Url())
err := operation.WithVolumeServerClient(false, dn.ServerAddress(), grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
_, err := volumeServerClient.VacuumVolumeCleanup(context.Background(), &volume_server_pb.VacuumVolumeCleanupRequest{
VolumeId: uint32(vid),
})
return err
})
if err != nil {
glog.Errorf("Error when cleaning up vacuum %d on %s: %v", vid, dn.Url(), err)
} else {
glog.V(0).Infof("Complete cleaning up vacuum %d on %s", vid, dn.Url())
}
}
}
func (t *Topology) Vacuum(grpcDialOption grpc.DialOption, garbageThreshold float64, maxParallelVacuumPerServer int, volumeId uint32, collection string, preallocate int64, automatic bool) {
// if there is vacuum going on, return immediately
swapped := atomic.CompareAndSwapInt64(&t.vacuumLockCounter, 0, 1)
if !swapped {
glog.V(0).Infof("Vacuum is already running")
return
}
defer atomic.StoreInt64(&t.vacuumLockCounter, 0)
// now only one vacuum process going on
glog.V(1).Infof("Start vacuum on demand with threshold: %f collection: %s volumeId: %d",
garbageThreshold, collection, volumeId)
for _, col := range t.collectionMap.Items() {
c := col.(*Collection)
if collection != "" && collection != c.Name {
continue
}
for _, vl := range c.storageType2VolumeLayout.Items() {
if vl != nil {
volumeLayout := vl.(*VolumeLayout)
if volumeId > 0 {
vid := needle.VolumeId(volumeId)
volumeLayout.accessLock.RLock()
locationList, ok := volumeLayout.vid2location[vid]
if ok {
locationList = locationList.Copy()
}
volumeLayout.accessLock.RUnlock()
if ok {
t.vacuumOneVolumeId(grpcDialOption, volumeLayout, c, garbageThreshold, locationList, vid, preallocate, false)
}
} else {
t.vacuumOneVolumeLayout(grpcDialOption, volumeLayout, c, garbageThreshold, maxParallelVacuumPerServer, preallocate, automatic)
}
}
if automatic && t.IsVacuumDisabled() {
break
}
}
if automatic && t.IsVacuumDisabled() {
glog.V(0).Infof("Vacuum is disabled")
break
}
}
}
func (t *Topology) vacuumOneVolumeLayout(grpcDialOption grpc.DialOption, volumeLayout *VolumeLayout, c *Collection, garbageThreshold float64, maxParallelVacuumPerServer int, preallocate int64, automatic bool) {
volumeLayout.accessLock.RLock()
todoVolumeMap := make(map[needle.VolumeId]*VolumeLocationList)
for vid, locationList := range volumeLayout.vid2location {
todoVolumeMap[vid] = locationList.Copy()
}
volumeLayout.accessLock.RUnlock()
// limiter for each volume server
limiter := make(map[NodeId]int)
var limiterLock sync.Mutex
for _, locationList := range todoVolumeMap {
for _, dn := range locationList.list {
if _, ok := limiter[dn.Id()]; !ok {
limiter[dn.Id()] = maxParallelVacuumPerServer
}
}
}
executor := util.NewLimitedConcurrentExecutor(100)
var wg sync.WaitGroup
for len(todoVolumeMap) > 0 {
pendingVolumeMap := make(map[needle.VolumeId]*VolumeLocationList)
for vid, locationList := range todoVolumeMap {
hasEnoughQuota := true
for _, dn := range locationList.list {
limiterLock.Lock()
quota := limiter[dn.Id()]
limiterLock.Unlock()
if quota <= 0 {
hasEnoughQuota = false
break
}
}
if !hasEnoughQuota {
pendingVolumeMap[vid] = locationList
continue
}
// debit the quota
for _, dn := range locationList.list {
limiterLock.Lock()
limiter[dn.Id()]--
limiterLock.Unlock()
}
wg.Add(1)
executor.Execute(func() {
defer wg.Done()
t.vacuumOneVolumeId(grpcDialOption, volumeLayout, c, garbageThreshold, locationList, vid, preallocate, true)
// credit the quota
for _, dn := range locationList.list {
limiterLock.Lock()
limiter[dn.Id()]++
limiterLock.Unlock()
}
})
if automatic && t.IsVacuumDisabled() {
break
}
}
if automatic && t.IsVacuumDisabled() {
break
}
if len(todoVolumeMap) == len(pendingVolumeMap) {
time.Sleep(10 * time.Second)
}
todoVolumeMap = pendingVolumeMap
}
wg.Wait()
}
// skipReadOnly is set by the background scan and all-volumes sweep, where a
// read-only flag usually means an unhealthy disk. An explicit volumeId clears
// it so a benignly read-only (full/oversized) volume can be reclaimed.
func (t *Topology) vacuumOneVolumeId(grpcDialOption grpc.DialOption, volumeLayout *VolumeLayout, c *Collection, garbageThreshold float64, locationList *VolumeLocationList, vid needle.VolumeId, preallocate int64, skipReadOnly bool) {
volumeLayout.accessLock.RLock()
isReadOnly := volumeLayout.vid2location[vid].AnyReadOnly()
isEnoughCopies := volumeLayout.enoughCopies(vid)
volumeLayout.accessLock.RUnlock()
if isReadOnly {
if skipReadOnly {
return
}
glog.V(0).Infof("vacuuming read-only volume %d on explicit request", vid)
}
if !isEnoughCopies {
glog.Warningf("skip vacuuming: not enough copies for volume:%d", vid)
return
}
glog.V(1).Infof("check vacuum on collection:%s volume:%d", c.Name, vid)
if vacuumLocationList, needVacuum := t.batchVacuumVolumeCheck(
grpcDialOption, vid, locationList, garbageThreshold); needVacuum {
if t.batchVacuumVolumeCompact(grpcDialOption, volumeLayout, vid, vacuumLocationList, preallocate) {
t.batchVacuumVolumeCommit(grpcDialOption, volumeLayout, vid, vacuumLocationList, locationList)
} else {
t.batchVacuumVolumeCleanup(grpcDialOption, volumeLayout, vid, vacuumLocationList)
}
}
}

View File

@@ -0,0 +1,485 @@
package topology
import (
"testing"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/util"
)
func digestTestNode(t *testing.T) (*Topology, *DataNode) {
t.Helper()
topo := NewTopology("digest", nil, 32*1024*1024*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("127.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 1000})
return topo, dn
}
func digestTestVolume(id uint32) *master_pb.VolumeInformationMessage {
return &master_pb.VolumeInformationMessage{
Id: id,
Size: 1024 * 1024,
Collection: "c",
FileCount: 10,
DeleteCount: 1,
DeletedByteCount: 128,
ReplicaPlacement: 0,
Version: 3,
CompactRevision: 1,
ModifiedAtSecond: 1700000000,
}
}
func TestVolumeDigestIsStableAcrossRepeatedHeartbeats(t *testing.T) {
topo, dn := digestTestNode(t)
volumes := []*master_pb.VolumeInformationMessage{digestTestVolume(1), digestTestVolume(2), digestTestVolume(3)}
topo.SyncDataNodeRegistration(volumes, dn)
first := dn.VolumeDigest()
if first == 0 {
t.Fatal("expected a non-zero digest for a node holding volumes")
}
for i := 0; i < 3; i++ {
topo.SyncDataNodeRegistration(volumes, dn)
if got := dn.VolumeDigest(); got != first {
t.Fatalf("heartbeat %d changed the digest with no change to report: %d != %d", i, got, first)
}
}
}
func TestVolumeDigestIsIndependentOfReportOrder(t *testing.T) {
topoA, dnA := digestTestNode(t)
topoA.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
digestTestVolume(1), digestTestVolume(2), digestTestVolume(3),
}, dnA)
topoB, dnB := digestTestNode(t)
topoB.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
digestTestVolume(3), digestTestVolume(1), digestTestVolume(2),
}, dnB)
if dnA.VolumeDigest() != dnB.VolumeDigest() {
t.Errorf("digest depends on report order: %d != %d", dnA.VolumeDigest(), dnB.VolumeDigest())
}
}
// Every field of VolumeInformationMessage has to reach the digest: one the hash
// skips is a change the master would never be told about. Enumerated from the
// message rather than listed here, so a field added later cannot quietly fall
// outside the digest while this still passes.
// Fields a server reports that the master deliberately does not keep, so a
// change to one alters nothing it holds and the digest is right not to move.
var notRetainedByTheMaster = map[string]string{
"remote_storage_key": "unique per volume, and the master decides nothing from it",
}
func TestVolumeDigestTracksEveryReportedField(t *testing.T) {
base := digestTestVolume(1)
baseInfo, err := storage.NewVolumeInfo(base)
if err != nil {
t.Fatal(err)
}
fields := base.ProtoReflect().Descriptor().Fields()
for i := 0; i < fields.Len(); i++ {
fd := fields.Get(i)
t.Run(string(fd.Name()), func(t *testing.T) {
why, skipped := notRetainedByTheMaster[string(fd.Name())]
candidates := distinctValuesFor(t, fd, base.ProtoReflect().Get(fd))
for _, candidate := range candidates {
changed := proto.Clone(base).(*master_pb.VolumeInformationMessage)
changed.ProtoReflect().Set(fd, candidate)
changedInfo, err := storage.NewVolumeInfo(changed)
if err != nil {
t.Fatal(err)
}
if baseInfo.ReportHash() != changedInfo.ReportHash() {
if skipped {
t.Errorf("%s moves the digest, but is listed as not retained by the master (%s)", fd.Name(), why)
}
return
}
}
if skipped {
return
}
t.Errorf("no change to %s moves the digest, so the master would never be told about one", fd.Name())
})
}
}
// distinctValuesFor offers values that differ from current. Several, because
// some fields are narrowed or normalised on the way into VolumeInfo and the
// smallest change to the wire value can land back on the stored one.
func distinctValuesFor(t *testing.T, fd protoreflect.FieldDescriptor, current protoreflect.Value) []protoreflect.Value {
t.Helper()
switch fd.Kind() {
case protoreflect.BoolKind:
return []protoreflect.Value{protoreflect.ValueOfBool(!current.Bool())}
case protoreflect.Uint32Kind:
return []protoreflect.Value{
protoreflect.ValueOfUint32(uint32(current.Uint()) + 1),
protoreflect.ValueOfUint32(uint32(current.Uint()) + 1<<8),
protoreflect.ValueOfUint32(uint32(current.Uint()) + 1<<16),
}
case protoreflect.Uint64Kind:
return []protoreflect.Value{protoreflect.ValueOfUint64(current.Uint() + 1)}
case protoreflect.Int64Kind:
return []protoreflect.Value{protoreflect.ValueOfInt64(current.Int() + 1)}
case protoreflect.StringKind:
return []protoreflect.Value{protoreflect.ValueOfString(current.String() + "x")}
}
t.Fatalf("field %s has kind %s, which this test does not know how to vary", fd.Name(), fd.Kind())
return nil
}
func TestVolumeDigestFollowsVolumeChanges(t *testing.T) {
topo, dn := digestTestNode(t)
volumes := []*master_pb.VolumeInformationMessage{digestTestVolume(1), digestTestVolume(2)}
topo.SyncDataNodeRegistration(volumes, dn)
original := dn.VolumeDigest()
grown := []*master_pb.VolumeInformationMessage{digestTestVolume(1), digestTestVolume(2)}
grown[1].Size += 4096
topo.SyncDataNodeRegistration(grown, dn)
if dn.VolumeDigest() == original {
t.Error("a volume that grew left the digest unchanged")
}
topo.SyncDataNodeRegistration(volumes, dn)
if dn.VolumeDigest() != original {
t.Error("reverting a volume did not restore the digest")
}
topo.SyncDataNodeRegistration(volumes[:1], dn)
if dn.VolumeDigest() == original {
t.Error("dropping a volume left the digest unchanged")
}
topo.SyncDataNodeRegistration(volumes, dn)
if dn.VolumeDigest() != original {
t.Error("restoring a dropped volume did not restore the digest")
}
}
func TestVolumeDigestEmptiesWithTheNode(t *testing.T) {
topo, dn := digestTestNode(t)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
digestTestVolume(1), digestTestVolume(2),
}, dn)
topo.SyncDataNodeRegistration(nil, dn)
if got := dn.VolumeDigest(); got != 0 {
t.Errorf("expected an empty node to digest to 0, got %d", got)
}
}
func TestVolumeDigestFollowsDeltaRegistration(t *testing.T) {
topo, dn := digestTestNode(t)
full := []*master_pb.VolumeInformationMessage{digestTestVolume(1), digestTestVolume(2)}
topo.SyncDataNodeRegistration(full, dn)
both := dn.VolumeDigest()
topo.IncrementalSyncDataNodeRegistration(nil, []*master_pb.VolumeShortInformationMessage{{Id: 2}}, dn)
if dn.VolumeDigest() == both {
t.Error("unmounting a volume left the digest unchanged")
}
topo.SyncDataNodeRegistration(full, dn)
if dn.VolumeDigest() != both {
t.Error("a full heartbeat did not restore the digest after an unmount")
}
}
// The point of the digest is not to detect that volumes changed -- in any live
// cluster some always have. It is to confirm that after applying the changes a
// heartbeat did carry, the master holds what the volume server holds. So a
// heartbeat reporting only the volumes that moved must still reconcile.
func TestVolumeDigestMatchesAfterApplyingOnlyChangedVolumes(t *testing.T) {
const total = 50
full := make([]*master_pb.VolumeInformationMessage, 0, total)
for i := 1; i <= total; i++ {
v := digestTestVolume(uint32(i))
v.ReadOnly = i > 5 // only the first few are writable, as in a tiered cluster
full = append(full, v)
}
topo, dn := digestTestNode(t)
topo.SyncDataNodeRegistration(full, dn)
// Three writable volumes take writes between two heartbeats.
changed := make([]storage.VolumeInfo, 0, 3)
for _, v := range full[:3] {
v.Size += 4096
v.FileCount++
v.ModifiedAtSecond += 5
vi, err := storage.NewVolumeInfo(v)
if err != nil {
t.Fatal(err)
}
changed = append(changed, vi)
}
// What the volume server would now report for its whole set.
reference, referenceNode := digestTestNode(t)
reference.SyncDataNodeRegistration(full, referenceNode)
want := referenceNode.VolumeDigest()
if dn.VolumeDigest() == want {
t.Fatal("expected the master to be behind before the changes are applied")
}
// The heartbeat carries three volumes, not fifty.
dn.DeltaUpdateVolumes(changed, nil)
if got := dn.VolumeDigest(); got != want {
t.Errorf("digest still disagrees after applying the reported changes: %d != %d", got, want)
}
}
// A volume that disappears without a delta is exactly what the full list exists
// to catch, and is the case the digest has to keep catching.
func TestVolumeDigestCatchesASilentlyLostVolume(t *testing.T) {
full := []*master_pb.VolumeInformationMessage{
digestTestVolume(1), digestTestVolume(2), digestTestVolume(3),
}
topo, dn := digestTestNode(t)
topo.SyncDataNodeRegistration(full, dn)
// The volume server no longer has volume 2 and never got to say so.
reference, referenceNode := digestTestNode(t)
reference.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{full[0], full[2]}, referenceNode)
if dn.VolumeDigest() == referenceNode.VolumeDigest() {
t.Error("a volume lost without a delta went undetected, which is what the full list is for")
}
}
// The disk map and the lookup index are maintained separately, and a disconnect
// racing a reconnect has been seen to drop a volume from the lookup index while
// leaving it on the node. The volume server's report is identical either way, so
// the heartbeat digest cannot see it and the master has to notice on its own.
func TestVolumeIndexDigestSeesLookupDivergence(t *testing.T) {
topo, dn := digestTestNode(t)
v := digestTestVolume(1)
v.Collection = "drr"
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{v}, dn)
if !dn.HasConsistentVolumeIndex() {
t.Fatal("a freshly registered node should have a consistent index")
}
reported := dn.VolumeDigest()
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("drr", rp, needle.EMPTY_TTL, types.HardDriveType)
vl.SetVolumeUnavailable(dn, needle.VolumeId(1))
if got := topo.Lookup("drr", needle.VolumeId(1)); got != nil {
t.Fatalf("expected the volume to have become unservable, got %v", got)
}
if _, err := dn.GetVolumesById(needle.VolumeId(1)); err != nil {
t.Fatalf("the volume should still be on the node: %v", err)
}
if dn.VolumeDigest() != reported {
t.Error("the reported digest should not move: the volume server sees no change")
}
if dn.HasConsistentVolumeIndex() {
t.Error("a volume held but not servable left the index digests agreeing, so nothing would repair it")
}
// The full heartbeat self-heal puts it back.
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{v}, dn)
if !dn.HasConsistentVolumeIndex() {
t.Error("the self-heal did not restore index consistency")
}
}
func TestVolumeIndexDigestFollowsNodeLifecycle(t *testing.T) {
topo, dn := digestTestNode(t)
full := []*master_pb.VolumeInformationMessage{digestTestVolume(1), digestTestVolume(2), digestTestVolume(3)}
topo.SyncDataNodeRegistration(full, dn)
if !dn.HasConsistentVolumeIndex() {
t.Fatal("registration left the indexes disagreeing")
}
topo.SyncDataNodeRegistration(full[:2], dn)
if !dn.HasConsistentVolumeIndex() {
t.Error("dropping a volume left the indexes disagreeing")
}
topo.IncrementalSyncDataNodeRegistration(
[]*master_pb.VolumeShortInformationMessage{{Id: 9}}, nil, dn)
if !dn.HasConsistentVolumeIndex() {
t.Error("a mount delta left the indexes disagreeing")
}
topo.IncrementalSyncDataNodeRegistration(
nil, []*master_pb.VolumeShortInformationMessage{{Id: 9}}, dn)
if !dn.HasConsistentVolumeIndex() {
t.Error("an unmount delta left the indexes disagreeing")
}
topo.UnRegisterDataNode(dn)
held, servable := dn.VolumeIndexDigests()
if held != 0 || servable != 0 {
t.Errorf("an unregistered node should hold nothing: held=%d servable=%d", held, servable)
}
}
// A volume id mounted on two disks of one server is reported twice with
// different disk ids, but the master keys volumes by id alone, so it keeps only
// one copy and its digest can never equal the server's. Resending the full list
// cannot fix that, so the node has to be excluded from digest comparison
// entirely rather than resend forever.
func TestVolumeDigestRefusesDuplicateVolumeIds(t *testing.T) {
topo, dn := digestTestNode(t)
first := digestTestVolume(1)
second := digestTestVolume(1)
second.DiskId = 1
second.Size = first.Size * 2
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{first, second}, dn)
if !dn.HasDuplicateVolumeIds() {
t.Fatal("a volume id reported twice went unnoticed, so the digest would be trusted and never reconcile")
}
firstInfo, err := storage.NewVolumeInfo(first)
if err != nil {
t.Fatal(err)
}
secondInfo, err := storage.NewVolumeInfo(second)
if err != nil {
t.Fatal(err)
}
if dn.VolumeDigest() == firstInfo.ReportHash()^secondInfo.ReportHash() {
t.Error("expected the master to be unable to represent both copies; if it now can, the guard is no longer needed")
}
// Once the stale twin is gone the node is comparable again.
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{first}, dn)
if dn.HasDuplicateVolumeIds() {
t.Error("the node stayed marked as duplicated after reporting a clean list")
}
if dn.VolumeDigest() != firstInfo.ReportHash() {
t.Error("digest did not settle on the surviving copy")
}
}
// Two volume servers can hold one address. GetOrCreateDataNode keys on the id a
// server reports and deliberately refuses to merge a new id onto an address an
// older node still claims, while the lookup list keys on address alone -- so
// registering the second server displaces the first from the lookup entry
// without either node being told. The digest has to follow the entry, not the
// node that was passed in.
func addressSharingNodes(t *testing.T) (*Topology, *DataNode, *DataNode) {
t.Helper()
topo := NewTopology("digest", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
counts := map[string]uint32{"": 1000}
old := rack.GetOrCreateDataNode("10.1.2.3", 8080, 18080, "", "n1", counts)
fresh := rack.GetOrCreateDataNode("10.1.2.3", 8080, 18080, "", "n2", counts)
if old == fresh {
t.Skip("address reuse no longer produces two nodes")
}
return topo, old, fresh
}
func TestVolumeIndexDigestFollowsDisplacedLookupEntry(t *testing.T) {
topo, old, fresh := addressSharingNodes(t)
v := digestTestVolume(1)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{v}, old)
if !old.HasConsistentVolumeIndex() {
t.Fatal("the first node should be consistent before it is displaced")
}
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{v}, fresh)
servable := map[*DataNode]bool{}
for _, dn := range topo.Lookup("", needle.VolumeId(1)) {
servable[dn] = true
}
if servable[old] || !servable[fresh] {
t.Fatalf("expected the lookup entry to move to the new node, got old=%v fresh=%v",
servable[old], servable[fresh])
}
if old.HasConsistentVolumeIndex() {
t.Error("the displaced node still holds the volume and can no longer serve it, so its index must read as inconsistent")
}
if !fresh.HasConsistentVolumeIndex() {
t.Error("the node the lookup entry now names reads as inconsistent")
}
}
func TestVolumeIndexDigestFollowsRemovedLookupEntry(t *testing.T) {
topo, old, fresh := addressSharingNodes(t)
v := digestTestVolume(1)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{v}, old)
// fresh shares old's address, so unregistering through it removes old's
// lookup entry. The digest must come off the node that was actually removed.
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)
vl.SetVolumeUnavailable(fresh, needle.VolumeId(1))
if got := topo.Lookup("c", needle.VolumeId(1)); got != nil {
t.Fatalf("expected the lookup entry to be gone, got %v", got)
}
if old.HasConsistentVolumeIndex() {
t.Error("the removed node still holds the volume and cannot serve it, so its index must read as inconsistent")
}
if _, servable := fresh.VolumeIndexDigests(); servable != 0 {
t.Error("the node that was merely passed in should never have gained the entry")
}
}
// The two ends must agree on real heartbeat data, not just on hand-built
// messages: the volume server hashes what it is about to send, the master
// hashes what it stored from it.
func TestMasterDigestMatchesWhatAVolumeServerReports(t *testing.T) {
dir := t.TempDir()
loc := storage.NewDiskLocation(dir, 100, util.MinFreeSpace{}, "", types.HardDriveType, nil,
stats.DefaultDiskIOProbeConfig())
for _, vid := range []needle.VolumeId{1, 2, 3} {
v, err := storage.NewVolume(loc.Directory, loc.IdxDirectory, "", vid, storage.NeedleMapInMemory,
&super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
if err != nil {
t.Fatal(err)
}
loc.SetVolume(vid, v)
}
reported := make([]*master_pb.VolumeInformationMessage, 0, 3)
var serverDigest uint64
for _, vid := range []needle.VolumeId{1, 2, 3} {
v, _ := loc.FindVolume(vid)
_, m := v.ToVolumeInformationMessage(nil)
if m == nil {
t.Fatalf("volume %d reported nothing", vid)
}
vi, err := storage.NewVolumeInfo(m)
if err != nil {
t.Fatal(err)
}
serverDigest ^= vi.ReportHash()
reported = append(reported, m)
}
topo, dn := digestTestNode(t)
topo.SyncDataNodeRegistration(reported, dn)
if got := dn.VolumeDigest(); got != serverDigest {
t.Errorf("master digest %d does not match the reporting server's %d", got, serverDigest)
}
}

View File

@@ -0,0 +1,112 @@
package topology
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
func diskMoveNode(t *testing.T) (*Topology, *DataNode) {
t.Helper()
topo := NewTopology("move", nil, 32*1024*1024*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("127.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 100, "ssd": 100})
return topo, dn
}
func diskMoveVolume(id uint32, diskType string, diskId uint32) *master_pb.VolumeInformationMessage {
return &master_pb.VolumeInformationMessage{
Id: id, Size: 1024, Collection: "c", Version: 3, DiskType: diskType, DiskId: diskId,
}
}
func heldCopies(dn *DataNode) int {
total := 0
for _, c := range dn.Children() {
total += c.(*Disk).VolumeCount()
}
return total
}
func TestVolumeMovedBetweenDisks(t *testing.T) {
for _, tc := range []struct {
name string
from, to string
}{
{"SameDiskType", "", ""},
{"DifferentDiskType", "", "ssd"},
{"BackAgain", "ssd", ""},
} {
t.Run(tc.name, func(t *testing.T) {
topo, dn := diskMoveNode(t)
topo.SyncDataNodeRegistration(
[]*master_pb.VolumeInformationMessage{diskMoveVolume(1, tc.from, 0)}, dn)
topo.SyncDataNodeRegistration(
[]*master_pb.VolumeInformationMessage{diskMoveVolume(1, tc.to, 1)}, dn)
if got := heldCopies(dn); got != 1 {
t.Errorf("master holds %d copies of a volume that moved, want 1", got)
}
stored, err := dn.GetVolumesById(needle.VolumeId(1))
if err != nil {
t.Fatalf("moved volume is no longer on the node: %v", err)
}
if stored.DiskType != tc.to || stored.DiskId != 1 {
t.Errorf("master has the volume on disk %q/%d, server reports %q/1",
stored.DiskType, stored.DiskId, tc.to)
}
if !dn.HasConsistentVolumeIndex() {
t.Error("the move left the lookup index disagreeing with the disks")
}
})
}
}
// A stale twin is two reports, not a move: dropping one would tell the master a
// replica vanished.
func TestVolumeReportedOnTwoDiskTypesIsKept(t *testing.T) {
topo, dn := diskMoveNode(t)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
diskMoveVolume(1, "", 0), diskMoveVolume(1, "ssd", 1),
}, dn)
if got := heldCopies(dn); got != 2 {
t.Errorf("master holds %d copies of a volume reported on two disks, want 2", got)
}
if dn.HasDuplicateVolumeIds() {
t.Error("a volume on two disk types is representable, so it should not disable digest comparison")
}
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{diskMoveVolume(1, "ssd", 1)}, dn)
if got := heldCopies(dn); got != 1 {
t.Errorf("master holds %d copies after the twin was unmounted, want 1", got)
}
}
func TestVolumeReportedTwiceOnOneDiskTypeIsFlagged(t *testing.T) {
topo, dn := diskMoveNode(t)
first := diskMoveVolume(1, "ssd", 0)
second := diskMoveVolume(1, "ssd", 1)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{first, second}, dn)
if !dn.HasDuplicateVolumeIds() {
t.Error("a volume reported twice on one disk type went unflagged")
}
}
func TestVolumeDigestSurvivesADiskMove(t *testing.T) {
topo, dn := diskMoveNode(t)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{diskMoveVolume(1, "", 0)}, dn)
moved := diskMoveVolume(1, "ssd", 1)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{moved}, dn)
reference, referenceNode := diskMoveNode(t)
reference.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{moved}, referenceNode)
if dn.VolumeDigest() != referenceNode.VolumeDigest() {
t.Errorf("after a disk move the digest is %d, a server holding only the moved volume reports %d",
dn.VolumeDigest(), referenceNode.VolumeDigest())
}
}

View File

@@ -0,0 +1,106 @@
package topology
import (
"errors"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
)
// VolumeFilter narrows a topology listing to the volumes a caller asked about,
// selecting only what is listed under a disk. A nil field filters nothing, and
// only nil does: the empty collection is a real one.
type VolumeFilter struct {
Collection *string
remoteStorageName *string
// VolumeIds selects the volumes it holds, and an empty one selects them all.
VolumeIds map[needle.VolumeId]struct{}
// nothing selects the topology alone, for a listing whose volumes travel
// in messages of their own.
nothing bool
}
// NoVolumes selects the topology and no volume in it.
func NoVolumes() VolumeFilter {
return VolumeFilter{nothing: true}
}
// NewVolumeFilter reads what a VolumeList request asked for, where empty and
// zero mean everything so a caller that forgets to narrow gets too much rather
// than the wrong thing. The exception is topology_only, which asks for no
// volumes at all: pairing it with a selector is refused rather than resolved,
// since either reading would silently drop half the request.
func NewVolumeFilter(req *master_pb.VolumeListRequest) (VolumeFilter, error) {
var filter VolumeFilter
if req.TopologyOnly {
if req.Collection != "" || len(req.VolumeIds) > 0 || req.DefaultCollectionOnly ||
req.RemoteStorageName != "" || req.LocalVolumeOnly {
return filter, errors.New("topology_only asks for no volumes, so it cannot be combined with collection, volume_ids, default_collection_only, remote_storage_name or local_volume_only")
}
filter.nothing = true
return filter, nil
}
switch {
case req.Collection != "":
collection := req.Collection
filter.Collection = &collection
case req.DefaultCollectionOnly:
defaultCollection := ""
filter.Collection = &defaultCollection
}
switch {
case req.RemoteStorageName != "":
filter.remoteStorageName = new(req.RemoteStorageName)
case req.LocalVolumeOnly:
filter.remoteStorageName = new("")
}
if len(req.VolumeIds) > 0 {
filter.VolumeIds = make(map[needle.VolumeId]struct{}, len(req.VolumeIds))
for _, volumeId := range req.VolumeIds {
filter.VolumeIds[needle.VolumeId(volumeId)] = struct{}{}
}
}
return filter, nil
}
// SelectsEverything lets a caller size its result for the whole disk up front.
func (f VolumeFilter) SelectsEverything() bool {
return !f.nothing && f.Collection == nil && len(f.VolumeIds) == 0 && f.remoteStorageName == nil
}
type volumeLike interface {
GetCollection() string
GetVolumeId() needle.VolumeId
GetRemoteStorageName() string
}
func (f VolumeFilter) matches(vi volumeLike) bool {
if f.nothing {
return false
}
if f.Collection != nil && *f.Collection != vi.GetCollection() {
return false
}
if f.remoteStorageName != nil {
pattern, name := *f.remoteStorageName, vi.GetRemoteStorageName()
// the empty name is the local volumes, which only asking for the empty
// name selects; even * leaves them out.
if name == "" && pattern != "" {
return false
}
if !wildcard.MatchesWildcard(pattern, name) {
return false
}
}
if len(f.VolumeIds) > 0 {
if _, ok := f.VolumeIds[vi.GetVolumeId()]; !ok {
return false
}
}
return true
}

View File

@@ -0,0 +1,406 @@
package topology
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
func filterTestTopology(t *testing.T) *Topology {
t.Helper()
topo := NewTopology("filter", nil, 32*1024*1024*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
{Id: 1, Collection: "", Size: 1000, Version: 3, DiskId: 2},
{Id: 2, Collection: "c", Size: 2000, Version: 3, DiskId: 2},
{Id: 3, Collection: "other", Size: 3000, Version: 3, DiskId: 2},
}, dn)
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 8, Collection: "c", EcIndexBits: 0x3fff, DiskId: 2},
{Id: 9, Collection: "other", EcIndexBits: 0x3fff, DiskId: 2},
}, dn)
return topo
}
// listed returns the volume and ec ids a filtered listing carries.
func listed(info *master_pb.TopologyInfo) (volumes []uint32, ecVolumes []uint32) {
for _, dc := range info.DataCenterInfos {
for _, rack := range dc.RackInfos {
for _, node := range rack.DataNodeInfos {
for _, disk := range node.DiskInfos {
for _, v := range disk.VolumeInfos {
volumes = append(volumes, v.Id)
}
for _, ec := range disk.EcShardInfos {
ecVolumes = append(ecVolumes, ec.Id)
}
}
}
}
}
return
}
func equalIds(got, want []uint32) bool {
if len(got) != len(want) {
return false
}
seen := map[uint32]int{}
for _, id := range got {
seen[id]++
}
for _, id := range want {
seen[id]--
}
for _, n := range seen {
if n != 0 {
return false
}
}
return true
}
func TestVolumeFilterSelects(t *testing.T) {
topo := filterTestTopology(t)
collection := func(name string) *string { return &name }
volumes := func(ids ...needle.VolumeId) map[needle.VolumeId]struct{} {
set := make(map[needle.VolumeId]struct{}, len(ids))
for _, id := range ids {
set[id] = struct{}{}
}
return set
}
for _, tc := range []struct {
name string
filter VolumeFilter
wantVolumes []uint32
wantEcVolumes []uint32
}{
{"no filter takes everything", VolumeFilter{}, []uint32{1, 2, 3}, []uint32{8, 9}},
{"one collection", VolumeFilter{Collection: collection("c")}, []uint32{2}, []uint32{8}},
// Asking for it must not read as asking for everything.
{"the default collection", VolumeFilter{Collection: collection("")}, []uint32{1}, nil},
{"a collection nothing is in", VolumeFilter{Collection: collection("none")}, nil, nil},
{"one volume", VolumeFilter{VolumeIds: volumes(3)}, []uint32{3}, nil},
{"one ec volume", VolumeFilter{VolumeIds: volumes(9)}, nil, []uint32{9}},
{"both, agreeing", VolumeFilter{Collection: collection("other"), VolumeIds: volumes(3)}, []uint32{3}, nil},
{"both, disagreeing", VolumeFilter{Collection: collection("c"), VolumeIds: volumes(3)}, nil, nil},
} {
t.Run(tc.name, func(t *testing.T) {
volumes, ecVolumes := listed(topo.ToTopologyInfo(tc.filter))
if !equalIds(volumes, tc.wantVolumes) {
t.Errorf("listed volumes %v, want %v", volumes, tc.wantVolumes)
}
if !equalIds(ecVolumes, tc.wantEcVolumes) {
t.Errorf("listed ec volumes %v, want %v", ecVolumes, tc.wantEcVolumes)
}
})
}
}
// an id nothing answers to narrows the listing rather than widening it.
func TestVolumeFilterVolumeIds(t *testing.T) {
topo := filterTestTopology(t)
for _, tc := range []struct {
name string
ids []needle.VolumeId
wantVolumes []uint32
wantEcVolumes []uint32
}{
// No id asked of the filter, so every volume is listed.
{"no id asked", nil, []uint32{1, 2, 3}, []uint32{8, 9}},
{"one regular volume asked", []needle.VolumeId{2}, []uint32{2}, nil},
{"one ec volume asked", []needle.VolumeId{8}, nil, []uint32{8}},
// Both kinds answer to the same set of ids.
{"both kinds asked", []needle.VolumeId{1, 8}, []uint32{1}, []uint32{8}},
{"every volume asked", []needle.VolumeId{1, 2, 3, 8, 9}, []uint32{1, 2, 3}, []uint32{8, 9}},
// An id nothing answers to must not read as asking for everything.
{"a missing id", []needle.VolumeId{7}, nil, nil},
{"a found and a missing id", []needle.VolumeId{2, 7}, []uint32{2}, nil},
} {
t.Run(tc.name, func(t *testing.T) {
ids := make(map[needle.VolumeId]struct{}, len(tc.ids))
for _, id := range tc.ids {
ids[id] = struct{}{}
}
volumes, ecVolumes := listed(topo.ToTopologyInfo(VolumeFilter{VolumeIds: ids}))
if !equalIds(volumes, tc.wantVolumes) {
t.Errorf("listed volumes %v, want %v", volumes, tc.wantVolumes)
}
if !equalIds(ecVolumes, tc.wantEcVolumes) {
t.Errorf("listed ec volumes %v, want %v", ecVolumes, tc.wantEcVolumes)
}
})
}
}
func TestVolumeFilterRemoteStorageNameWildcards(t *testing.T) {
topo := NewTopology("filter", nil, 32*1024*1024*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
{Id: 11, Collection: "c", Size: 100, Version: 3, DiskId: 3, RemoteStorageName: "s3.backup"},
{Id: 12, Collection: "c", Size: 100, Version: 3, DiskId: 3, RemoteStorageName: "s3.archive"},
{Id: 13, Collection: "c", Size: 100, Version: 3, DiskId: 3, RemoteStorageName: "gcs.cold"},
{Id: 14, Collection: "c", Size: 100, Version: 3, DiskId: 3},
}, dn)
for _, tc := range []struct {
name string
f VolumeFilter
want []uint32
}{
// A nil name asks for every volume, tiered or local alike.
{"no remote storage asked", VolumeFilter{}, []uint32{11, 12, 13, 14}},
{"an exact name", VolumeFilter{remoteStorageName: new("s3.backup")}, []uint32{11}},
{"a wildcarded storage", VolumeFilter{remoteStorageName: new("s3.*")}, []uint32{11, 12}},
{"a wildcarded tier", VolumeFilter{remoteStorageName: new("*.cold")}, []uint32{13}},
{"every tiered volume", VolumeFilter{remoteStorageName: new("*")}, []uint32{11, 12, 13}},
// Not a wildcard, so it matches nothing rather than everything.
{"a name no volume is on", VolumeFilter{remoteStorageName: new("s3.other")}, nil},
// The local volumes are the ones with no remote storage name.
{"the local ones only", VolumeFilter{remoteStorageName: new("")}, []uint32{14}},
} {
t.Run(tc.name, func(t *testing.T) {
volumes, ecVolumes := listed(topo.ToTopologyInfo(tc.f))
if !equalIds(volumes, tc.want) {
t.Errorf("listed volumes %v, want %v", volumes, tc.want)
}
if len(ecVolumes) != 0 {
t.Errorf("listed ec volumes %v, want none", ecVolumes)
}
})
}
}
// A filter never changes which disks are reported or what they say about
// themselves.
func TestVolumeFilterKeepsTheTopology(t *testing.T) {
topo := filterTestTopology(t)
full := topo.ToTopologyInfo(VolumeFilter{})
name := "c"
narrowed := topo.ToTopologyInfo(VolumeFilter{Collection: &name})
for _, info := range []*master_pb.TopologyInfo{full, narrowed} {
if len(info.DataCenterInfos) != 1 || len(info.DataCenterInfos[0].RackInfos) != 1 {
t.Fatalf("listing lost its topology: %v", info)
}
}
fullDisk := full.DataCenterInfos[0].RackInfos[0].DataNodeInfos[0].DiskInfos[""]
narrowedDisk := narrowed.DataCenterInfos[0].RackInfos[0].DataNodeInfos[0].DiskInfos[""]
if narrowedDisk == nil {
t.Fatal("the filtered listing dropped the disk")
}
if narrowedDisk.VolumeCount != fullDisk.VolumeCount {
t.Errorf("volume count %d, want the disk's own %d", narrowedDisk.VolumeCount, fullDisk.VolumeCount)
}
if narrowedDisk.MaxVolumeCount != fullDisk.MaxVolumeCount {
t.Errorf("max volume count %d, want %d", narrowedDisk.MaxVolumeCount, fullDisk.MaxVolumeCount)
}
if narrowedDisk.DiskId != fullDisk.DiskId {
t.Errorf("disk id %d, want %d", narrowedDisk.DiskId, fullDisk.DiskId)
}
}
// So a caller that finds nothing can tell an empty answer from a missing disk.
func TestVolumeFilterKeepsTheDiskIdWhenNothingMatches(t *testing.T) {
topo := filterTestTopology(t)
name := "none"
info := topo.ToTopologyInfo(VolumeFilter{Collection: &name})
disk := info.DataCenterInfos[0].RackInfos[0].DataNodeInfos[0].DiskInfos[""]
if len(disk.VolumeInfos) != 0 {
t.Fatalf("expected nothing listed, got %d volumes", len(disk.VolumeInfos))
}
if disk.DiskId != 2 {
t.Errorf("reported disk id %d, want the one its volumes are on", disk.DiskId)
}
}
func TestNewVolumeFilterReadsTheRequest(t *testing.T) {
collectionOf := func(f VolumeFilter) string {
if f.Collection == nil {
return "<every>"
}
return *f.Collection
}
for _, tc := range []struct {
name string
request *master_pb.VolumeListRequest
want string
}{
// A caller passing through its own "" is answered too much, not wrongly.
{"an empty request", &master_pb.VolumeListRequest{}, "<every>"},
{"an empty collection", &master_pb.VolumeListRequest{Collection: ""}, "<every>"},
{"a named collection", &master_pb.VolumeListRequest{Collection: "c"}, "c"},
{"the default collection", &master_pb.VolumeListRequest{DefaultCollectionOnly: true}, ""},
// Contradictory, so the more specific of the two wins.
{"both", &master_pb.VolumeListRequest{Collection: "c", DefaultCollectionOnly: true}, "c"},
} {
t.Run(tc.name, func(t *testing.T) {
filter, err := NewVolumeFilter(tc.request)
if err != nil {
t.Fatal(err)
}
if got := collectionOf(filter); got != tc.want {
t.Errorf("selected collection %q, want %q", got, tc.want)
}
})
}
}
func TestNewVolumeFilterReadsTheRemoteStorageRequest(t *testing.T) {
remoteStorageOf := func(f VolumeFilter) string {
if f.remoteStorageName == nil {
return "<every>"
}
return *f.remoteStorageName
}
for _, tc := range []struct {
name string
request *master_pb.VolumeListRequest
want string
}{
// A caller passing through its own "" is answered too much, not wrongly.
{"an empty request", &master_pb.VolumeListRequest{}, "<every>"},
{"an empty remote storage name", &master_pb.VolumeListRequest{RemoteStorageName: ""}, "<every>"},
{"a named remote storage", &master_pb.VolumeListRequest{RemoteStorageName: "s3.backup"}, "s3.backup"},
// A wildcard is the caller's pattern, carried across untouched.
{"a wildcarded storage", &master_pb.VolumeListRequest{RemoteStorageName: "*"}, "*"},
// The one storage (local) the empty string cannot name.
{"the local volumes only", &master_pb.VolumeListRequest{LocalVolumeOnly: true}, ""},
// Contradictory, so the more specific of the two wins.
{"both", &master_pb.VolumeListRequest{RemoteStorageName: "s3.backup", LocalVolumeOnly: true}, "s3.backup"},
} {
t.Run(tc.name, func(t *testing.T) {
filter, err := NewVolumeFilter(tc.request)
if err != nil {
t.Fatal(err)
}
if got := remoteStorageOf(filter); got != tc.want {
t.Errorf("selected remote storage %q, want %q", got, tc.want)
}
})
}
}
func TestNewVolumeFilterReadsTheVolumeIds(t *testing.T) {
idsOf := func(f VolumeFilter) []uint32 {
ids := make([]uint32, 0, len(f.VolumeIds))
for id := range f.VolumeIds {
ids = append(ids, uint32(id))
}
return ids
}
for _, tc := range []struct {
name string
request *master_pb.VolumeListRequest
want []uint32
}{
// No id asked of the request, so every volume is listed.
{"an empty request", &master_pb.VolumeListRequest{}, nil},
{"one id", &master_pb.VolumeListRequest{VolumeIds: []uint32{7}}, []uint32{7}},
{"a list of ids", &master_pb.VolumeListRequest{VolumeIds: []uint32{2, 8}}, []uint32{2, 8}},
// Asking twice for a volume selects it once.
{"duplicates", &master_pb.VolumeListRequest{VolumeIds: []uint32{2, 2}}, []uint32{2}},
// The list is taken literally: no volume answers to zero, so a zero in
// it narrows the listing rather than widening it.
{"a zero among the ids", &master_pb.VolumeListRequest{VolumeIds: []uint32{0}}, []uint32{0}},
} {
t.Run(tc.name, func(t *testing.T) {
filter, err := NewVolumeFilter(tc.request)
if err != nil {
t.Fatal(err)
}
if got := idsOf(filter); !equalIds(got, tc.want) {
t.Errorf("selected volume ids %v, want %v", got, tc.want)
}
})
}
}
// topology_only leaves out the ec shards too, not just the volumes; the disks
// and their counters stay.
func TestNewVolumeFilterReadsTopologyOnly(t *testing.T) {
topo := filterTestTopology(t)
filter, err := NewVolumeFilter(&master_pb.VolumeListRequest{TopologyOnly: true})
if err != nil {
t.Fatal(err)
}
info := topo.ToTopologyInfo(filter)
volumes, ecVolumes := listed(info)
if len(volumes) != 0 || len(ecVolumes) != 0 {
t.Errorf("listed volumes %v and ec volumes %v, want neither", volumes, ecVolumes)
}
full := topo.ToTopologyInfo(VolumeFilter{})
fullDisk := full.DataCenterInfos[0].RackInfos[0].DataNodeInfos[0].DiskInfos[""]
disk := info.DataCenterInfos[0].RackInfos[0].DataNodeInfos[0].DiskInfos[""]
if disk == nil {
t.Fatal("the topology_only listing dropped the disk")
}
if disk.VolumeCount != fullDisk.VolumeCount || disk.MaxVolumeCount != fullDisk.MaxVolumeCount {
t.Errorf("disk counters %d/%d, want the full listing's %d/%d",
disk.VolumeCount, disk.MaxVolumeCount, fullDisk.VolumeCount, fullDisk.MaxVolumeCount)
}
}
// Selecting volumes in a request that asks for none is a caller bug, refused
// rather than resolved either way in silence.
func TestNewVolumeFilterRefusesTopologyOnlyWithSelectors(t *testing.T) {
for _, tc := range []struct {
name string
request *master_pb.VolumeListRequest
}{
{"a collection", &master_pb.VolumeListRequest{TopologyOnly: true, Collection: "c"}},
{"volume ids", &master_pb.VolumeListRequest{TopologyOnly: true, VolumeIds: []uint32{1}}},
{"the default collection", &master_pb.VolumeListRequest{TopologyOnly: true, DefaultCollectionOnly: true}},
{"a remote storage", &master_pb.VolumeListRequest{TopologyOnly: true, RemoteStorageName: "s3.backup"}},
{"the local volumes", &master_pb.VolumeListRequest{TopologyOnly: true, LocalVolumeOnly: true}},
} {
t.Run(tc.name, func(t *testing.T) {
if _, err := NewVolumeFilter(tc.request); err == nil {
t.Error("expected the contradiction refused, got a filter")
}
})
}
}
// Everything a listing reads off a disk must be read under its lock, including
// how much room to reserve, or a heartbeat writing the map races it.
func TestVolumeFilterListsWhileVolumesChange(t *testing.T) {
topo := filterTestTopology(t)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 100})
stop := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
for i := 0; ; i++ {
select {
case <-stop:
return
default:
}
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
{Id: uint32(100 + i%50), Collection: "c", Size: 1, Version: 3, DiskId: 2},
}, dn)
}
}()
name := "c"
for i := 0; i < 200; i++ {
topo.ToTopologyInfo(VolumeFilter{})
topo.ToTopologyInfo(VolumeFilter{Collection: &name})
}
close(stop)
<-done
}

View File

@@ -0,0 +1,415 @@
package topology
import (
"encoding/json"
"fmt"
"math/rand/v2"
"reflect"
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"google.golang.org/grpc"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
/*
This package is created to resolve these replica placement issues:
1. growth factor for each replica level, e.g., add 10 volumes for 1 copy, 20 volumes for 2 copies, 30 volumes for 3 copies
2. in time of tight storage, how to reduce replica level
3. optimizing for hot data on faster disk, cold data on cheaper storage,
4. volume allocation for each bucket
*/
type VolumeGrowRequest struct {
Option *VolumeGrowOption
Count uint32
Force bool
Reason string
}
func (vg *VolumeGrowRequest) Equals(req *VolumeGrowRequest) bool {
return reflect.DeepEqual(vg.Option, req.Option) && vg.Count == req.Count && vg.Force == req.Force
}
type volumeGrowthStrategy struct {
Copy1Count uint32
Copy2Count uint32
Copy3Count uint32
CopyOtherCount uint32
Threshold float64
}
var (
VolumeGrowStrategy = volumeGrowthStrategy{
Copy1Count: 7,
Copy2Count: 6,
Copy3Count: 3,
CopyOtherCount: 1,
Threshold: 0.9,
}
)
type VolumeGrowOption struct {
Collection string `json:"collection,omitempty"`
ReplicaPlacement *super_block.ReplicaPlacement `json:"replication,omitempty"`
Ttl *needle.TTL `json:"ttl,omitempty"`
DiskType types.DiskType `json:"disk,omitempty"`
Preallocate int64 `json:"preallocate,omitempty"`
DataCenter string `json:"dataCenter,omitempty"`
Rack string `json:"rack,omitempty"`
DataNode string `json:"dataNode,omitempty"`
MemoryMapMaxSizeMb uint32 `json:"memoryMapMaxSizeMb,omitempty"`
Version uint32 `json:"version,omitempty"`
}
type VolumeGrowth struct {
accessLock sync.Mutex
}
// VolumeGrowReservation tracks capacity reservations for a volume creation operation
type VolumeGrowReservation struct {
servers []*DataNode
reservationIds []string
diskType types.DiskType
}
// releaseAllReservations releases all reservations in this volume grow operation
func (vgr *VolumeGrowReservation) releaseAllReservations() {
for i, server := range vgr.servers {
if i < len(vgr.reservationIds) && vgr.reservationIds[i] != "" {
server.ReleaseReservedCapacity(vgr.reservationIds[i])
}
}
}
func (o *VolumeGrowOption) String() string {
blob, _ := json.Marshal(o)
return string(blob)
}
func NewDefaultVolumeGrowth() *VolumeGrowth {
return &VolumeGrowth{}
}
// VolumeGrowthCountForCopies returns the configured number of logical volumes
// to create at once for a given replica copy count (master.volume_growth.copy_N).
func VolumeGrowthCountForCopies(copyCount int) uint32 {
switch copyCount {
case 1:
return VolumeGrowStrategy.Copy1Count
case 2:
return VolumeGrowStrategy.Copy2Count
case 3:
return VolumeGrowStrategy.Copy3Count
default:
return VolumeGrowStrategy.CopyOtherCount
}
}
// one replication type may need rp.GetCopyCount() actual volumes
// given copyCount, how many logical volumes to create
func (vg *VolumeGrowth) findVolumeCount(copyCount int) (count uint32) {
return VolumeGrowthCountForCopies(copyCount)
}
func (vg *VolumeGrowth) AutomaticGrowByType(option *VolumeGrowOption, grpcDialOption grpc.DialOption, topo *Topology, targetCount uint32) (result []*master_pb.VolumeLocation, err error) {
if targetCount == 0 {
targetCount = vg.findVolumeCount(option.ReplicaPlacement.GetCopyCount())
}
result, err = vg.GrowByCountAndType(grpcDialOption, targetCount, option, topo)
if len(result) > 0 && len(result)%option.ReplicaPlacement.GetCopyCount() == 0 {
return result, nil
}
return result, err
}
func (vg *VolumeGrowth) GrowByCountAndType(grpcDialOption grpc.DialOption, targetCount uint32, option *VolumeGrowOption, topo *Topology) (result []*master_pb.VolumeLocation, err error) {
vg.accessLock.Lock()
defer vg.accessLock.Unlock()
for i := uint32(0); i < targetCount; i++ {
if res, e := vg.findAndGrow(grpcDialOption, topo, option); e == nil {
result = append(result, res...)
stats.MasterVolumeCreationCounter.WithLabelValues("success").Inc()
} else {
glog.V(0).Infof("create %d volume, created %d: %v", targetCount, len(result), e)
stats.MasterVolumeCreationCounter.WithLabelValues("failure").Inc()
return result, e
}
}
return
}
func (vg *VolumeGrowth) findAndGrow(grpcDialOption grpc.DialOption, topo *Topology, option *VolumeGrowOption) (result []*master_pb.VolumeLocation, err error) {
// Wait for warmup to complete before searching for slots so the
// topology has all volume servers registered.
for topo.IsWarmingUp() {
glog.V(0).Infof("wait for volume servers to join back")
time.Sleep(topo.WarmupDuration() / WarmupPulseMultiplier / 2)
}
servers, reservation, e := vg.findEmptySlotsForOneVolume(topo, option, true) // use reservations
if e != nil {
return nil, e
}
// Ensure reservations are released if anything goes wrong
defer func() {
if err != nil && reservation != nil {
reservation.releaseAllReservations()
}
}()
vid, raftErr := topo.NextVolumeId()
if raftErr != nil {
return nil, raftErr
}
if err = vg.grow(grpcDialOption, topo, vid, option, reservation, servers...); err == nil {
for _, server := range servers {
result = append(result, &master_pb.VolumeLocation{
Url: server.Url(),
PublicUrl: server.PublicUrl,
DataCenter: server.GetDataCenterId(),
GrpcPort: uint32(server.GrpcPort),
NewVids: []uint32{uint32(vid)},
})
}
}
return
}
// 1. find the main data node
// 1.1 collect all data nodes that have 1 slots
// 2.2 collect all racks that have rp.SameRackCount+1
// 2.2 collect all data centers that have DiffRackCount+rp.SameRackCount+1
// 2. find rest data nodes
// If useReservations is true, reserves capacity on each server and returns reservation info
func (vg *VolumeGrowth) findEmptySlotsForOneVolume(topo *Topology, option *VolumeGrowOption, useReservations bool) (servers []*DataNode, reservation *VolumeGrowReservation, err error) {
//find main datacenter and other data centers
rp := option.ReplicaPlacement
// Track tentative reservations to make the process atomic
var tentativeReservation *VolumeGrowReservation
// Select appropriate functions based on useReservations flag
var availableSpaceFunc func(Node, *VolumeGrowOption) int64
var reserveOneVolumeFunc func(Node, int64, *VolumeGrowOption) (*DataNode, error)
if useReservations {
// Initialize tentative reservation tracking
tentativeReservation = &VolumeGrowReservation{
servers: make([]*DataNode, 0),
reservationIds: make([]string, 0),
diskType: option.DiskType,
}
// For reservations, we make actual reservations during node selection
availableSpaceFunc = func(node Node, option *VolumeGrowOption) int64 {
return node.AvailableSpaceForReservation(option)
}
reserveOneVolumeFunc = func(node Node, r int64, option *VolumeGrowOption) (*DataNode, error) {
return node.ReserveOneVolumeForReservation(r, option)
}
} else {
availableSpaceFunc = func(node Node, option *VolumeGrowOption) int64 {
return node.AvailableSpaceFor(option)
}
reserveOneVolumeFunc = func(node Node, r int64, option *VolumeGrowOption) (*DataNode, error) {
return node.ReserveOneVolume(r, option)
}
}
// Ensure cleanup of partial reservations on error
defer func() {
if err != nil && tentativeReservation != nil {
tentativeReservation.releaseAllReservations()
}
}()
mainDataCenter, otherDataCenters, dc_err := topo.PickNodesByWeight(rp.DiffDataCenterCount+1, option, func(node Node) error {
if option.DataCenter != "" && node.IsDataCenter() && node.Id() != NodeId(option.DataCenter) {
return fmt.Errorf("Not matching preferred data center:%s", option.DataCenter)
}
if len(node.Children()) < rp.DiffRackCount+1 {
return fmt.Errorf("Only has %d racks, not enough for %d.", len(node.Children()), rp.DiffRackCount+1)
}
if availableSpaceFunc(node, option) < int64(rp.DiffRackCount+rp.SameRackCount+1) {
return fmt.Errorf("Free:%d < Expected:%d", availableSpaceFunc(node, option), rp.DiffRackCount+rp.SameRackCount+1)
}
possibleRacksCount := 0
for _, rack := range node.Children() {
possibleDataNodesCount := 0
for _, n := range rack.Children() {
if availableSpaceFunc(n, option) >= 1 {
possibleDataNodesCount++
}
}
if possibleDataNodesCount >= rp.SameRackCount+1 {
possibleRacksCount++
}
}
if possibleRacksCount < rp.DiffRackCount+1 {
return fmt.Errorf("Only has %d racks with more than %d free data nodes, not enough for %d.", possibleRacksCount, rp.SameRackCount+1, rp.DiffRackCount+1)
}
return nil
})
if dc_err != nil {
return nil, nil, dc_err
}
//find main rack and other racks
mainRack, otherRacks, rackErr := mainDataCenter.(*DataCenter).PickNodesByWeight(rp.DiffRackCount+1, option, func(node Node) error {
if option.Rack != "" && node.IsRack() && node.Id() != NodeId(option.Rack) {
return fmt.Errorf("Not matching preferred rack:%s", option.Rack)
}
if availableSpaceFunc(node, option) < int64(rp.SameRackCount+1) {
return fmt.Errorf("Free:%d < Expected:%d", availableSpaceFunc(node, option), rp.SameRackCount+1)
}
if len(node.Children()) < rp.SameRackCount+1 {
// a bit faster way to test free racks
return fmt.Errorf("Only has %d data nodes, not enough for %d.", len(node.Children()), rp.SameRackCount+1)
}
possibleDataNodesCount := 0
for _, n := range node.Children() {
if availableSpaceFunc(n, option) >= 1 {
possibleDataNodesCount++
}
}
if possibleDataNodesCount < rp.SameRackCount+1 {
return fmt.Errorf("Only has %d data nodes with a slot, not enough for %d.", possibleDataNodesCount, rp.SameRackCount+1)
}
return nil
})
if rackErr != nil {
return nil, nil, rackErr
}
//find main server and other servers
mainServer, otherServers, serverErr := mainRack.(*Rack).PickNodesByWeight(rp.SameRackCount+1, option, func(node Node) error {
if option.DataNode != "" && node.IsDataNode() && node.Id() != NodeId(option.DataNode) {
return fmt.Errorf("Not matching preferred data node:%s", option.DataNode)
}
if useReservations {
// For reservations, atomically check and reserve capacity
if node.IsDataNode() {
reservationId, success := node.TryReserveCapacity(option.DiskType, 1)
if !success {
return fmt.Errorf("Cannot reserve capacity on node %s", node.Id())
}
// Track the reservation for later cleanup if needed
tentativeReservation.servers = append(tentativeReservation.servers, node.(*DataNode))
tentativeReservation.reservationIds = append(tentativeReservation.reservationIds, reservationId)
} else if availableSpaceFunc(node, option) < 1 {
return fmt.Errorf("Free:%d < Expected:%d", availableSpaceFunc(node, option), 1)
}
} else if availableSpaceFunc(node, option) < 1 {
return fmt.Errorf("Free:%d < Expected:%d", availableSpaceFunc(node, option), 1)
}
return nil
})
if serverErr != nil {
return nil, nil, serverErr
}
servers = append(servers, mainServer.(*DataNode))
for _, server := range otherServers {
servers = append(servers, server.(*DataNode))
}
for _, rack := range otherRacks {
r := rand.Int64N(availableSpaceFunc(rack, option))
if server, e := reserveOneVolumeFunc(rack, r, option); e == nil {
servers = append(servers, server)
// If using reservations, also make a reservation on the selected server
if useReservations {
reservationId, success := server.TryReserveCapacity(option.DiskType, 1)
if !success {
return servers, nil, fmt.Errorf("failed to reserve capacity on server %s from other rack", server.Id())
}
tentativeReservation.servers = append(tentativeReservation.servers, server)
tentativeReservation.reservationIds = append(tentativeReservation.reservationIds, reservationId)
}
} else {
return servers, nil, e
}
}
for _, datacenter := range otherDataCenters {
r := rand.Int64N(availableSpaceFunc(datacenter, option))
if server, e := reserveOneVolumeFunc(datacenter, r, option); e == nil {
servers = append(servers, server)
// If using reservations, also make a reservation on the selected server
if useReservations {
reservationId, success := server.TryReserveCapacity(option.DiskType, 1)
if !success {
return servers, nil, fmt.Errorf("failed to reserve capacity on server %s from other datacenter", server.Id())
}
tentativeReservation.servers = append(tentativeReservation.servers, server)
tentativeReservation.reservationIds = append(tentativeReservation.reservationIds, reservationId)
}
} else {
return servers, nil, e
}
}
// If reservations were made, return the tentative reservation
if useReservations && tentativeReservation != nil {
reservation = tentativeReservation
glog.V(1).Infof("Successfully reserved capacity on %d servers for volume creation", len(servers))
}
return servers, reservation, nil
}
// grow creates volumes on the provided servers, optionally managing capacity reservations
func (vg *VolumeGrowth) grow(grpcDialOption grpc.DialOption, topo *Topology, vid needle.VolumeId, option *VolumeGrowOption, reservation *VolumeGrowReservation, servers ...*DataNode) (growErr error) {
var createdVolumes []storage.VolumeInfo
for _, server := range servers {
if err := AllocateVolume(server, grpcDialOption, vid, option); err == nil {
createdVolumes = append(createdVolumes, storage.VolumeInfo{
Id: vid,
Size: 0,
Collection: option.Collection,
ReplicaPlacement: option.ReplicaPlacement,
Ttl: option.Ttl,
Version: needle.Version(option.Version),
DiskType: option.DiskType.String(),
ModifiedAtSecond: time.Now().Unix(),
})
glog.V(0).Infof("Created Volume %d on %s", vid, server.NodeImpl.String())
} else {
glog.Warningf("Failed to assign volume %d on %s: %v", vid, server.NodeImpl.String(), err)
growErr = fmt.Errorf("failed to assign volume %d on %s: %v", vid, server.NodeImpl.String(), err)
break
}
}
if growErr == nil {
for i, vi := range createdVolumes {
server := servers[i]
server.AddProvisionalVolume(vi)
topo.RegisterVolumeLayout(vi, server)
glog.V(0).Infof("Registered Volume %d on %s", vid, server.NodeImpl.String())
}
// Release reservations on success since volumes are now registered
if reservation != nil {
reservation.releaseAllReservations()
}
} else {
// cleaning up created volume replicas
for i, vi := range createdVolumes {
server := servers[i]
if err := DeleteVolume(server, grpcDialOption, vi.Id); err != nil {
glog.Warningf("Failed to clean up volume %d on %s", vid, server.NodeImpl.String())
}
}
// Reservations will be released by the caller in case of failure
}
return growErr
}

View File

@@ -0,0 +1,295 @@
package topology
import (
"sync"
"sync/atomic"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
// MockGrpcDialOption simulates grpc connection for testing
type MockGrpcDialOption struct{}
func TestVolumeGrowth_ReservationBasedAllocation(t *testing.T) {
// Create test topology with single server for predictable behavior
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
// Create data center and rack
dc := NewDataCenter("dc1")
topo.LinkChildNode(dc)
rack := NewRack("rack1")
dc.LinkChildNode(rack)
// Create single data node with limited capacity
dn := NewDataNode("server1")
rack.LinkChildNode(dn)
// Set up disk with limited capacity (only 5 volumes)
disk := NewDisk(types.HardDriveType.String())
disk.diskUsages.getOrCreateDisk(types.HardDriveType).maxVolumeCount = 5
dn.LinkChildNode(disk)
// Test volume growth with reservation
vg := NewDefaultVolumeGrowth()
rp, _ := super_block.NewReplicaPlacementFromString("000") // Single copy (no replicas)
option := &VolumeGrowOption{
Collection: "test",
ReplicaPlacement: rp,
DiskType: types.HardDriveType,
}
// Try to create volumes and verify reservations work
for i := 0; i < 5; i++ {
servers, reservation, err := vg.findEmptySlotsForOneVolume(topo, option, true)
if err != nil {
t.Errorf("Failed to find slots with reservation on iteration %d: %v", i, err)
continue
}
if len(servers) != 1 {
t.Errorf("Expected 1 server for replica placement 000, got %d", len(servers))
}
if len(reservation.reservationIds) != 1 {
t.Errorf("Expected 1 reservation ID, got %d", len(reservation.reservationIds))
}
// Verify the reservation is on our expected server
server := servers[0]
if server != dn {
t.Errorf("Expected volume to be allocated on server1, got %s", server.Id())
}
// Check available space before and after reservation
availableBeforeCreation := server.AvailableSpaceFor(option)
expectedBefore := int64(5 - i)
if availableBeforeCreation != expectedBefore {
t.Errorf("Iteration %d: Expected %d base available space, got %d", i, expectedBefore, availableBeforeCreation)
}
// Simulate successful volume creation
// Acquire lock briefly to access children map, then release before updating
dn.RLock()
disk := dn.children[NodeId(types.HardDriveType.String())].(*Disk)
dn.RUnlock()
deltaDiskUsage := &DiskUsageCounts{
volumeCount: 1,
}
disk.UpAdjustDiskUsageDelta(types.HardDriveType, deltaDiskUsage)
// Release reservation after successful creation
reservation.releaseAllReservations()
// Verify available space after creation
availableAfterCreation := server.AvailableSpaceFor(option)
expectedAfter := int64(5 - i - 1)
if availableAfterCreation != expectedAfter {
t.Errorf("Iteration %d: Expected %d available space after creation, got %d", i, expectedAfter, availableAfterCreation)
}
}
// After 5 volumes, should have no more capacity
_, _, err := vg.findEmptySlotsForOneVolume(topo, option, true)
if err == nil {
t.Error("Expected volume allocation to fail when server is at capacity")
}
}
func TestVolumeGrowth_ConcurrentAllocationPreventsRaceCondition(t *testing.T) {
// Create test topology with very limited capacity
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := NewDataCenter("dc1")
topo.LinkChildNode(dc)
rack := NewRack("rack1")
dc.LinkChildNode(rack)
// Single data node with capacity for only 5 volumes
dn := NewDataNode("server1")
rack.LinkChildNode(dn)
disk := NewDisk(types.HardDriveType.String())
disk.diskUsages.getOrCreateDisk(types.HardDriveType).maxVolumeCount = 5
dn.LinkChildNode(disk)
vg := NewDefaultVolumeGrowth()
rp, _ := super_block.NewReplicaPlacementFromString("000") // Single copy (no replicas)
option := &VolumeGrowOption{
Collection: "test",
ReplicaPlacement: rp,
DiskType: types.HardDriveType,
}
// Simulate concurrent volume creation attempts
const concurrentRequests = 10
var wg sync.WaitGroup
var successCount, failureCount atomic.Int32
var commitMutex sync.Mutex // Ensures atomic commit of volume creation + reservation release
for i := 0; i < concurrentRequests; i++ {
wg.Add(1)
go func(requestId int) {
defer wg.Done()
_, reservation, err := vg.findEmptySlotsForOneVolume(topo, option, true)
if err != nil {
failureCount.Add(1)
t.Logf("Request %d failed as expected: %v", requestId, err)
} else {
successCount.Add(1)
t.Logf("Request %d succeeded, got reservation", requestId)
// Simulate completion: increment volume count BEFORE releasing reservation
if reservation != nil {
commitMutex.Lock()
// First, increment the volume count to reflect the created volume
// Acquire lock briefly to access children map, then release before updating
dn.RLock()
disk := dn.children[NodeId(types.HardDriveType.String())].(*Disk)
dn.RUnlock()
deltaDiskUsage := &DiskUsageCounts{
volumeCount: 1,
}
disk.UpAdjustDiskUsageDelta(types.HardDriveType, deltaDiskUsage)
// Then release the reservation
reservation.releaseAllReservations()
commitMutex.Unlock()
}
}
}(i)
}
wg.Wait()
// Collect results
successes := successCount.Load()
failures := failureCount.Load()
total := successes + failures
if total != concurrentRequests {
t.Fatalf("Expected %d total attempts recorded, got %d", concurrentRequests, total)
}
// At most the available capacity should succeed
const capacity = 5
if successes > capacity {
t.Errorf("Expected no more than %d successful reservations, got %d", capacity, successes)
}
// We should see at least the remaining attempts fail
minExpectedFailures := concurrentRequests - capacity
if failures < int32(minExpectedFailures) {
t.Errorf("Expected at least %d failed reservations, got %d", minExpectedFailures, failures)
}
// Verify final state matches the number of successful allocations
finalAvailable := dn.AvailableSpaceFor(option)
expectedAvailable := int64(capacity - successes)
if finalAvailable != expectedAvailable {
t.Errorf("Expected %d available space after allocations, got %d", expectedAvailable, finalAvailable)
}
t.Logf("Concurrent test completed: %d successes, %d failures", successes, failures)
}
func TestVolumeGrowth_ReservationFailureRollback(t *testing.T) {
// Create topology with multiple servers, but limited total capacity
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := NewDataCenter("dc1")
topo.LinkChildNode(dc)
rack := NewRack("rack1")
dc.LinkChildNode(rack)
// Create two servers with different available capacity
dn1 := NewDataNode("server1")
dn2 := NewDataNode("server2")
rack.LinkChildNode(dn1)
rack.LinkChildNode(dn2)
// Server 1: 5 available slots
disk1 := NewDisk(types.HardDriveType.String())
disk1.diskUsages.getOrCreateDisk(types.HardDriveType).maxVolumeCount = 5
dn1.LinkChildNode(disk1)
// Server 2: 0 available slots (full)
disk2 := NewDisk(types.HardDriveType.String())
diskUsage2 := disk2.diskUsages.getOrCreateDisk(types.HardDriveType)
diskUsage2.maxVolumeCount = 5
diskUsage2.volumeCount = 5
dn2.LinkChildNode(disk2)
vg := NewDefaultVolumeGrowth()
rp, _ := super_block.NewReplicaPlacementFromString("010") // requires 2 replicas
option := &VolumeGrowOption{
Collection: "test",
ReplicaPlacement: rp,
DiskType: types.HardDriveType,
}
// This should fail because we can't satisfy replica requirements
// (need 2 servers but only 1 has space)
_, _, err := vg.findEmptySlotsForOneVolume(topo, option, true)
if err == nil {
t.Error("Expected reservation to fail due to insufficient replica capacity")
}
// Verify no reservations are left hanging
available1 := dn1.AvailableSpaceForReservation(option)
if available1 != 5 {
t.Errorf("Expected server1 to have all capacity available after failed reservation, got %d", available1)
}
available2 := dn2.AvailableSpaceForReservation(option)
if available2 != 0 {
t.Errorf("Expected server2 to have no capacity available, got %d", available2)
}
}
func TestVolumeGrowth_ReservationTimeout(t *testing.T) {
dn := NewDataNode("server1")
diskType := types.HardDriveType
// Set up capacity
diskUsage := dn.diskUsages.getOrCreateDisk(diskType)
diskUsage.maxVolumeCount = 5
// Create a reservation
reservationId, success := dn.TryReserveCapacity(diskType, 2)
if !success {
t.Fatal("Expected successful reservation")
}
// Manually set the reservation time to simulate old reservation
dn.capacityReservations.Lock()
if reservation, exists := dn.capacityReservations.reservations[reservationId]; exists {
reservation.createdAt = time.Now().Add(-10 * time.Minute)
}
dn.capacityReservations.Unlock()
// Try another reservation - this should trigger cleanup and succeed
_, success = dn.TryReserveCapacity(diskType, 3)
if !success {
t.Error("Expected reservation to succeed after cleanup of expired reservation")
}
// Original reservation should be cleaned up
option := &VolumeGrowOption{DiskType: diskType}
available := dn.AvailableSpaceForReservation(option)
if available != 2 { // 5 - 3 = 2
t.Errorf("Expected 2 available slots after cleanup and new reservation, got %d", available)
}
}

View File

@@ -0,0 +1,514 @@
package topology
import (
"encoding/json"
"fmt"
"testing"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
)
var topologyLayout = `
{
"dc1":{
"rack1":{
"server111":{
"volumes":[
{"id":1, "size":12312},
{"id":2, "size":12312},
{"id":3, "size":12312}
],
"limit":3
},
"server112":{
"volumes":[
{"id":4, "size":12312},
{"id":5, "size":12312},
{"id":6, "size":12312}
],
"limit":10
}
},
"rack2":{
"server121":{
"volumes":[
{"id":4, "size":12312},
{"id":5, "size":12312},
{"id":6, "size":12312}
],
"limit":4
},
"server122":{
"volumes":[],
"limit":4
},
"server123":{
"volumes":[
{"id":2, "size":12312},
{"id":3, "size":12312},
{"id":4, "size":12312}
],
"limit":5
}
}
},
"dc2":{
},
"dc3":{
"rack2":{
"server321":{
"volumes":[
{"id":1, "size":12312},
{"id":3, "size":12312},
{"id":5, "size":12312}
],
"limit":4
}
}
}
}
`
func setup(topologyLayout string) *Topology {
var data interface{}
err := json.Unmarshal([]byte(topologyLayout), &data)
if err != nil {
fmt.Println("error:", err)
}
fmt.Println("data:", data)
//need to connect all nodes first before server adding volumes
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
mTopology := data.(map[string]interface{})
for dcKey, dcValue := range mTopology {
dc := NewDataCenter(dcKey)
dcMap := dcValue.(map[string]interface{})
topo.LinkChildNode(dc)
for rackKey, rackValue := range dcMap {
dcRack := NewRack(rackKey)
rackMap := rackValue.(map[string]interface{})
dc.LinkChildNode(dcRack)
for serverKey, serverValue := range rackMap {
server := NewDataNode(serverKey)
serverMap := serverValue.(map[string]interface{})
if ip, ok := serverMap["ip"]; ok {
server.Ip = ip.(string)
}
dcRack.LinkChildNode(server)
for _, v := range serverMap["volumes"].([]interface{}) {
m := v.(map[string]interface{})
vi := storage.VolumeInfo{
Id: needle.VolumeId(int64(m["id"].(float64))),
Size: uint64(m["size"].(float64)),
Version: needle.GetCurrentVersion(),
}
if mVal, ok := m["collection"]; ok {
vi.Collection = mVal.(string)
}
if mVal, ok := m["replication"]; ok {
rp, _ := super_block.NewReplicaPlacementFromString(mVal.(string))
vi.ReplicaPlacement = rp
}
if vi.ReplicaPlacement != nil {
vl := topo.GetVolumeLayout(vi.Collection, vi.ReplicaPlacement, needle.EMPTY_TTL, types.HardDriveType)
vl.RegisterVolume(&vi, server)
vl.setVolumeWritable(vi.Id)
}
server.AddOrUpdateVolume(vi)
}
disk := server.getOrCreateDisk("")
disk.UpAdjustDiskUsageDelta("", &DiskUsageCounts{
maxVolumeCount: int64(serverMap["limit"].(float64)),
})
}
}
}
return topo
}
func TestFindEmptySlotsForOneVolume(t *testing.T) {
topo := setup(topologyLayout)
vg := NewDefaultVolumeGrowth()
rp, _ := super_block.NewReplicaPlacementFromString("002")
volumeGrowOption := &VolumeGrowOption{
Collection: "",
ReplicaPlacement: rp,
DataCenter: "dc1",
Rack: "",
DataNode: "",
}
servers, _, err := vg.findEmptySlotsForOneVolume(topo, volumeGrowOption, false)
if err != nil {
fmt.Println("finding empty slots error :", err)
t.Fail()
}
for _, server := range servers {
fmt.Println("assigned node :", server.Id())
}
}
var topologyLayout2 = `
{
"dc1":{
"rack1":{
"server111":{
"volumes":[
{"id":1, "size":12312},
{"id":2, "size":12312},
{"id":3, "size":12312}
],
"limit":300
},
"server112":{
"volumes":[
{"id":4, "size":12312},
{"id":5, "size":12312},
{"id":6, "size":12312}
],
"limit":300
},
"server113":{
"volumes":[],
"limit":300
},
"server114":{
"volumes":[],
"limit":300
},
"server115":{
"volumes":[],
"limit":300
},
"server116":{
"volumes":[],
"limit":300
}
},
"rack2":{
"server121":{
"volumes":[
{"id":4, "size":12312},
{"id":5, "size":12312},
{"id":6, "size":12312}
],
"limit":300
},
"server122":{
"volumes":[],
"limit":300
},
"server123":{
"volumes":[
{"id":2, "size":12312},
{"id":3, "size":12312},
{"id":4, "size":12312}
],
"limit":300
},
"server124":{
"volumes":[],
"limit":300
},
"server125":{
"volumes":[],
"limit":300
},
"server126":{
"volumes":[],
"limit":300
}
},
"rack3":{
"server131":{
"volumes":[],
"limit":300
},
"server132":{
"volumes":[],
"limit":300
},
"server133":{
"volumes":[],
"limit":300
},
"server134":{
"volumes":[],
"limit":300
},
"server135":{
"volumes":[],
"limit":300
},
"server136":{
"volumes":[],
"limit":300
}
}
}
}
`
func TestReplication011(t *testing.T) {
topo := setup(topologyLayout2)
vg := NewDefaultVolumeGrowth()
rp, _ := super_block.NewReplicaPlacementFromString("011")
volumeGrowOption := &VolumeGrowOption{
Collection: "MAIL",
ReplicaPlacement: rp,
DataCenter: "dc1",
Rack: "",
DataNode: "",
}
servers, _, err := vg.findEmptySlotsForOneVolume(topo, volumeGrowOption, false)
if err != nil {
fmt.Println("finding empty slots error :", err)
t.Fail()
}
for _, server := range servers {
fmt.Println("assigned node :", server.Id())
}
}
var topologyLayout3 = `
{
"dc1":{
"rack1":{
"server111":{
"volumes":[],
"limit":2000
}
}
},
"dc2":{
"rack2":{
"server222":{
"volumes":[],
"limit":2000
}
}
},
"dc3":{
"rack3":{
"server333":{
"volumes":[],
"limit":1000
}
}
},
"dc4":{
"rack4":{
"server444":{
"volumes":[],
"limit":1000
}
}
},
"dc5":{
"rack5":{
"server555":{
"volumes":[],
"limit":500
}
}
},
"dc6":{
"rack6":{
"server666":{
"volumes":[],
"limit":500
}
}
}
}
`
func TestFindEmptySlotsForOneVolumeScheduleByWeight(t *testing.T) {
topo := setup(topologyLayout3)
vg := NewDefaultVolumeGrowth()
rp, _ := super_block.NewReplicaPlacementFromString("100")
volumeGrowOption := &VolumeGrowOption{
Collection: "Weight",
ReplicaPlacement: rp,
DataCenter: "",
Rack: "",
DataNode: "",
}
distribution := map[NodeId]int{}
// assign 1000 volumes
for i := 0; i < 1000; i++ {
servers, _, err := vg.findEmptySlotsForOneVolume(topo, volumeGrowOption, false)
if err != nil {
fmt.Println("finding empty slots error :", err)
t.Fail()
}
for _, server := range servers {
// fmt.Println("assigned node :", server.Id())
if _, ok := distribution[server.id]; !ok {
distribution[server.id] = 0
}
distribution[server.id] += 1
}
}
for k, v := range distribution {
fmt.Printf("%s : %d\n", k, v)
}
}
var topologyLayout4 = `
{
"dc1":{
"rack1":{
"serverdc111":{
"ip": "127.0.0.1",
"volumes":[
{"id":1, "size":12312, "collection":"test", "replication":"001"},
{"id":2, "size":12312, "collection":"test", "replication":"100"},
{"id":4, "size":12312, "collection":"test", "replication":"100"},
{"id":6, "size":12312, "collection":"test", "replication":"010"}
],
"limit":100
}
}
},
"dc2":{
"rack1":{
"serverdc211":{
"ip": "127.0.0.2",
"volumes":[
{"id":2, "size":12312, "collection":"test", "replication":"100"},
{"id":3, "size":12312, "collection":"test", "replication":"010"},
{"id":5, "size":12312, "collection":"test", "replication":"001"},
{"id":6, "size":12312, "collection":"test", "replication":"010"}
],
"limit":100
}
}
},
"dc3":{
"rack1":{
"serverdc311":{
"ip": "127.0.0.3",
"volumes":[
{"id":1, "size":12312, "collection":"test", "replication":"001"},
{"id":3, "size":12312, "collection":"test", "replication":"010"},
{"id":4, "size":12312, "collection":"test", "replication":"100"},
{"id":5, "size":12312, "collection":"test", "replication":"001"}
],
"limit":100
}
}
}
}
`
func TestPickForWrite(t *testing.T) {
topo := setup(topologyLayout4)
volumeGrowOption := &VolumeGrowOption{
Collection: "test",
DataCenter: "",
Rack: "",
DataNode: "",
}
VolumeGrowStrategy.Threshold = 0.9
for _, rpStr := range []string{"001", "010", "100"} {
rp, _ := super_block.NewReplicaPlacementFromString(rpStr)
vl := topo.GetVolumeLayout("test", rp, needle.EMPTY_TTL, types.HardDriveType)
volumeGrowOption.ReplicaPlacement = rp
for _, dc := range []string{"", "dc1", "dc2", "dc3", "dc0"} {
volumeGrowOption.DataCenter = dc
for _, r := range []string{""} {
volumeGrowOption.Rack = r
for _, dn := range []string{""} {
if dc == "" && dn != "" {
continue
}
volumeGrowOption.DataNode = dn
// Small expectedDataSize hint: this test iterates many times
// and the layout uses a 32KB volume size limit. With hint=0
// (default 1MB pendingDelta), RecordAssign would exceed the
// limit on the first call and eagerly remove the volume
// from writable. Keep the hint tiny so effectiveSize stays
// below the limit across all iterations.
fileId, count, _, shouldGrow, err := topo.PickForWrite(1, volumeGrowOption, vl, 1024)
if dc == "dc0" {
if err == nil || count != 0 || !shouldGrow {
fmt.Println(dc, r, dn, "pick for write should be with error")
t.Fail()
}
} else if err != nil {
fmt.Println(dc, r, dn, "pick for write error :", err)
t.Fail()
} else if count == 0 {
fmt.Println(dc, r, dn, "pick for write count is zero")
t.Fail()
} else if len(fileId) == 0 {
fmt.Println(dc, r, dn, "pick for write file id is empty")
t.Fail()
} else if shouldGrow {
fmt.Println(dc, r, dn, "pick for write error : not should grow")
t.Fail()
}
// Also verify with a non-zero expectedDataSize hint
if dc != "dc0" {
fileId2, count2, _, shouldGrow2, err2 := topo.PickForWrite(1, volumeGrowOption, vl, 1024)
if err2 != nil {
fmt.Println(dc, r, dn, "pick for write with size hint error:", err2)
t.Fail()
} else if count2 == 0 || len(fileId2) == 0 || shouldGrow2 {
fmt.Println(dc, r, dn, "pick for write with size hint unexpected result")
t.Fail()
}
}
}
}
}
}
}
// TestPickNodesByWeightPrefersDistinctHosts: a rack runs two physical machines
// with two volume servers each. Picking two nodes (e.g. for a same-rack replica
// pair) must land them on distinct hosts so a single machine failure cannot take
// out both replicas, even though the four data nodes are independent.
func TestPickNodesByWeightPrefersDistinctHosts(t *testing.T) {
rack := NewRack("rack1")
mk := func(id, ip string, maxVol int64) *DataNode {
dn := NewDataNode(id)
dn.Ip = ip
rack.LinkChildNode(dn)
disk := dn.getOrCreateDisk("")
disk.UpAdjustDiskUsageDelta("", &DiskUsageCounts{maxVolumeCount: maxVol})
return dn
}
mk("s1a", "10.0.0.1", 10)
mk("s1b", "10.0.0.1", 10)
mk("s2a", "10.0.0.2", 10)
mk("s2b", "10.0.0.2", 10)
option := &VolumeGrowOption{DiskType: types.HardDriveType}
// Selection is weighted-random, so exercise it repeatedly: with two machines
// available the chosen pair must always span both.
for i := 0; i < 50; i++ {
first, rest, err := rack.PickNodesByWeight(2, option, func(node Node) error { return nil })
if err != nil {
t.Fatalf("PickNodesByWeight: %v", err)
}
if len(rest) != 1 {
t.Fatalf("got %d rest nodes, want 1", len(rest))
}
if nodeHost(first) == nodeHost(rest[0]) {
t.Errorf("both picks on host %s; expected distinct machines", nodeHost(first))
}
}
}

View File

@@ -0,0 +1,84 @@
package topology
import (
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
// VolumeIdDigestHash spreads a volume id across the whole 64-bit range so that
// xoring a set of them together does not collide for the near-consecutive ids a
// cluster actually allocates.
func VolumeIdDigestHash(vid needle.VolumeId) uint64 {
h := uint64(vid) + 0x9E3779B97F4A7C15
h = (h ^ (h >> 30)) * 0xBF58476D1CE4E5B9
h = (h ^ (h >> 27)) * 0x94D049BB133111EB
return h ^ (h >> 31)
}
// VolumeIndexDigests returns the digest of the volumes the node's disks hold and
// the digest of the volumes the lookup index will serve from that node.
//
// The master keeps those two indexes separately, and a disconnect racing a
// reconnect has been seen to drop a volume from the lookup index while leaving
// it on the node: the volume stays visible in volume.list and the admin UI while
// LookupVolume answers "volume id not found". A volume server cannot detect that
// -- its own report is identical either way -- so the master has to notice it
// on its own rather than rely on the heartbeat digest.
func (dn *DataNode) VolumeIndexDigests() (held, servable uint64) {
dn.RLock()
for _, c := range dn.children {
held ^= c.(*Disk).VolumeIdDigest()
}
dn.RUnlock()
return held, dn.lookupDigest.Load()
}
// HasConsistentVolumeIndex reports whether every volume on the node is reachable
// through the lookup index, and nothing else is.
func (dn *DataNode) HasConsistentVolumeIndex() bool {
held, servable := dn.VolumeIndexDigests()
return held == servable
}
// releaseLookupOwnership clears the lookup digest bit of every location this
// layout serves. Used when the layout is dropped wholesale with its collection
// rather than volume by volume.
func (vl *VolumeLayout) releaseLookupOwnership() {
vl.accessLock.Lock()
defer vl.accessLock.Unlock()
for vid, location := range vl.vid2location {
for _, dn := range location.list {
moveLookupOwnership(vid, dn, nil)
}
}
vl.vid2location = make(map[needle.VolumeId]*VolumeLocationList)
vl.dropped = true
}
// moveLookupOwnership transfers the digest bit for vid from the node a lookup
// entry used to name to the node it now names. Either may be nil, for an entry
// being created or dropped.
func moveLookupOwnership(vid needle.VolumeId, from, to *DataNode) {
if from == to {
return
}
if from != nil {
from.trackLookupChange(vid)
}
if to != nil {
to.trackLookupChange(vid)
}
}
// trackLookupChange records that vid became reachable through this node, or
// stopped being: xor is its own inverse, so both are the same update. Volume
// layouts for different collections share the node, so it cannot assume the
// caller's lock.
func (dn *DataNode) trackLookupChange(vid needle.VolumeId) {
delta := VolumeIdDigestHash(vid)
for {
old := dn.lookupDigest.Load()
if dn.lookupDigest.CompareAndSwap(old, old^delta) {
return
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,332 @@
package topology
import (
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
// A volume removed by the heartbeat capacity path (not RecordAssign) must
// still recover once it shrinks — only works if SetVolumeCapacityFull stamps
// fullSince.
func TestSetVolumeCapacityFullStampsFullSinceAndRecovers(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":4000, "replication":"000"}
],
"limit":10
}
}
}
}
`
topo, vl := setupPickTest(t, layout, 10000)
initialActive := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount
initialWritables, _ := vl.GetWritableVolumeCount()
if initialWritables != 1 {
t.Fatalf("expected 1 writable volume initially, got %d", initialWritables)
}
if !vl.SetVolumeCapacityFull(1) {
t.Fatalf("SetVolumeCapacityFull should report the volume was writable")
}
vl.AdjustActiveVolumeCountForFull(1)
if w, _ := vl.GetWritableVolumeCount(); w != 0 {
t.Fatalf("expected 0 writable after capacity full, got %d", w)
}
if vl.sizeTracking[1].fullSince.IsZero() {
t.Fatalf("expected SetVolumeCapacityFull to stamp fullSince")
}
// No recovery before capacityRecoveryDelay.
advanceSizeTrackingClock(vl, 1, 3*time.Second)
if vl.UpdateVolumeSize(1, 4000, 0, true) {
t.Fatalf("recovery should not fire before capacityRecoveryDelay")
}
// After the delay, a smaller size restores it.
advanceSizeTrackingClock(vl, 1, capacityRecoveryDelay)
if !vl.UpdateVolumeSize(1, 4000, 0, true) {
t.Fatalf("expected volume to recover to writable after shrinking")
}
vl.AdjustActiveVolumeCountAfterRecovery(1)
if w, _ := vl.GetWritableVolumeCount(); w != initialWritables {
t.Fatalf("expected %d writable after recovery, got %d", initialWritables, w)
}
if got := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount; got != initialActive {
t.Fatalf("expected activeVolumeCount restored to %d, got %d", initialActive, got)
}
if !vl.sizeTracking[1].fullSince.IsZero() {
t.Fatalf("expected fullSince cleared after recovery")
}
}
func TestSetVolumeAvailableRestoresActiveCountForCapacityFullVolume(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":4000, "replication":"000"}
],
"limit":10
}
}
}
}
`
topo, vl := setupPickTest(t, layout, 10000)
initialActive := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount
if !vl.SetVolumeCapacityFull(1) {
t.Fatalf("SetVolumeCapacityFull should report the volume was writable")
}
vl.AdjustActiveVolumeCountForFull(1)
if got := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount; got != initialActive-1 {
t.Fatalf("expected activeVolumeCount decremented to %d, got %d", initialActive-1, got)
}
vl.accessLock.RLock()
dn := vl.vid2location[1].list[0]
vl.accessLock.RUnlock()
if !vl.SetVolumeAvailable(dn, 1, false, false) {
t.Fatalf("SetVolumeAvailable should report the volume became writable")
}
if got := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount; got != initialActive {
t.Fatalf("expected SetVolumeAvailable to restore activeVolumeCount to %d, got %d", initialActive, got)
}
if !vl.sizeTracking[1].fullSince.IsZero() {
t.Fatalf("expected SetVolumeAvailable to clear fullSince after restoring activeVolumeCount")
}
advanceSizeTrackingClock(vl, 1, capacityRecoveryDelay+time.Second)
if vl.UpdateVolumeSize(1, 4000, 0, true) {
t.Fatalf("heartbeat recovery should not fire after SetVolumeAvailable already restored the volume")
}
if got := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount; got != initialActive {
t.Fatalf("expected activeVolumeCount to remain %d, got %d", initialActive, got)
}
}
// A volume marked full by pending estimates takes no writes, so no heartbeat
// reports it again and only the periodic decay can bring it back.
func TestDecayQuietVolumeSizesRecoversPhantomFullVolume(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":4000, "replication":"000"}
],
"limit":10
}
}
}
}
`
topo, vl := setupPickTest(t, layout, 10000)
VolumeGrowStrategy.Threshold = 0.9
initialActive := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount
if !vl.RecordAssign(1, 20000) {
t.Fatalf("expected RecordAssign to remove the volume from writable")
}
vl.AdjustActiveVolumeCountForFull(1)
if w, _ := vl.GetWritableVolumeCount(); w != 0 {
t.Fatalf("expected 0 writable after the pending estimate filled the volume, got %d", w)
}
recovered := false
for i := 0; i < 10 && !recovered; i++ {
advanceSizeTrackingClock(vl, 1, capacityRecoveryDelay+time.Second)
topo.DecayQuietVolumeSizes()
w, _ := vl.GetWritableVolumeCount()
recovered = w == 1
}
if !recovered {
t.Fatalf("the periodic decay never returned the volume to the writable list")
}
if got := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount; got != initialActive {
t.Fatalf("expected activeVolumeCount restored to %d, got %d", initialActive, got)
}
if !vl.sizeTracking[1].fullSince.IsZero() {
t.Fatalf("expected fullSince cleared after recovery")
}
}
// The decay must not consume the replica-dedup window: a real report landing
// right behind it carries a size no volume server will send again, since only
// a volume whose content changed is reported at all.
func TestDecayQuietVolumeSizesKeepsTheNextHeartbeat(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":4000, "replication":"000"}
],
"limit":10
}
}
}
}
`
topo, vl := setupPickTest(t, layout, 10000)
vl.RecordAssign(1, 2000)
advanceSizeTrackingClock(vl, 1, 30*time.Second)
topo.DecayQuietVolumeSizes()
vl.UpdateVolumeSize(1, 6000, 7, true)
vl.accessLock.RLock()
defer vl.accessLock.RUnlock()
if got := vl.sizeTracking[1].reportedSize; got != 6000 {
t.Errorf("the heartbeat after a decay left reportedSize at %d, want the reported 6000", got)
}
if got := vl.sizeTracking[1].compactRevision; got != 7 {
t.Errorf("the heartbeat after a decay left compactRevision at %d, want the reported 7", got)
}
}
// The decay picks its volumes under a read lock and replays them under a write
// one. A compaction heartbeat landing in that gap must survive, so the replay
// takes the record as it stands rather than the size the pass set out with —
// the arguments below stand in for that older snapshot.
func TestDecayQuietVolumeSizesDoesNotRollBackAHeartbeat(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":9000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
vl.RecordAssign(1, 500)
// The heartbeat that wins the race: a compaction shrank the volume.
vl.UpdateVolumeSize(1, 2000, 3, true)
// Far enough behind that the dedup window no longer covers for the replay,
// which is the only case where reading the record under the lock is what
// saves the report.
advanceSizeTrackingClock(vl, 1, 3*time.Second)
vl.UpdateVolumeSize(1, 9000, 0, false)
vl.accessLock.RLock()
defer vl.accessLock.RUnlock()
if got := vl.sizeTracking[1].reportedSize; got != 2000 {
t.Errorf("the decay rolled reportedSize back to %d, want the compacted 2000", got)
}
if got := vl.sizeTracking[1].compactRevision; got != 3 {
t.Errorf("the decay rolled compactRevision back to %d, want the reported 3", got)
}
if got := vl.sizeTracking[1].effectiveSize; got > 2000 {
t.Errorf("effectiveSize %d outgrew the compacted size the heartbeat reported", got)
}
}
// The decay stands in for a report that never came, so a heartbeat arriving
// after the pass chose the volume takes its place rather than adding to it:
// halving twice in one cycle forgets pending bytes the volume has yet to write.
func TestDecayQuietVolumeSizesYieldsToAHeartbeatItRaced(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":4000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
vl.RecordAssign(1, 4000)
advanceSizeTrackingClock(vl, 1, 30*time.Second)
// The heartbeat wins the race and does the one decay this cycle owes.
vl.UpdateVolumeSize(1, 4000, 0, true)
vl.accessLock.RLock()
afterHeartbeat := vl.sizeTracking[1].effectiveSize
vl.accessLock.RUnlock()
if afterHeartbeat != 6000 {
t.Fatalf("the heartbeat left effectiveSize at %d, want 6000", afterHeartbeat)
}
vl.UpdateVolumeSize(1, 0, 0, false)
vl.accessLock.RLock()
defer vl.accessLock.RUnlock()
if got := vl.sizeTracking[1].effectiveSize; got != afterHeartbeat {
t.Errorf("the decay halved again to %d, want the heartbeat's %d left alone", got, afterHeartbeat)
}
}
// A volume the disk really did fill can never recover through the decay, so it
// must leave the candidate set instead of costing a write lock every pulse for
// the rest of its life.
func TestDecayQuietVolumeSizesSkipsAGenuinelyFullVolume(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":10000, "replication":"000"},
{"id":2, "size":4000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// Volume 1 is full on disk; volume 2 only looks full to the estimate.
vl.SetVolumeCapacityFull(1)
vl.RecordAssign(2, 6000)
advanceSizeTrackingClock(vl, 1, 30*time.Second)
advanceSizeTrackingClock(vl, 2, 30*time.Second)
candidates := vl.quietDecayCandidates(10 * time.Second)
for _, vid := range candidates {
if vid == 1 {
t.Errorf("the decay keeps taking the write lock for volume 1, which the disk really did fill")
}
}
if len(candidates) != 1 || candidates[0] != 2 {
t.Errorf("candidates %v, want only the phantom-full volume 2", candidates)
}
vl.DecayQuietVolumeSizes(10 * time.Second)
vl.accessLock.RLock()
defer vl.accessLock.RUnlock()
if got := vl.sizeTracking[2].effectiveSize; got != 7000 {
t.Errorf("the phantom-full volume was left at %d, want it decayed to 7000", got)
}
}

View File

@@ -0,0 +1,261 @@
package topology
import (
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
func TestGetPendingSize(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":1000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// Initially no pending
if p := vl.GetPendingSize(1); p != 0 {
t.Fatalf("expected 0 pending, got %d", p)
}
// RecordAssign increases pending
vl.RecordAssign(1, 5000)
if p := vl.GetPendingSize(1); p != 5000 {
t.Fatalf("expected 5000 pending, got %d", p)
}
// UpdateVolumeSize (heartbeat) decays pending
vl.UpdateVolumeSize(1, 3000, 0, true)
// effective was 6000, reported 3000 → decay to 3000 + (6000-3000)/2 = 4500
// pending = 4500 - 3000 = 1500
if p := vl.GetPendingSize(1); p != 1500 {
t.Fatalf("expected 1500 pending after decay, got %d", p)
}
}
func TestGetPendingSize_CompactionResets(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":5000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// Add large pending
vl.RecordAssign(1, 4000)
if p := vl.GetPendingSize(1); p != 4000 {
t.Fatalf("expected 4000 pending, got %d", p)
}
// Compaction happens — size drops from 5000 to 2000, revision changes.
// Without compaction awareness, decay would give: 2000 + (9000-2000)/2 = 5500.
// With compaction awareness, vid2size resets to 2000 (the real size).
vl.UpdateVolumeSize(1, 2000, 1, true) // revision 0 → 1
if p := vl.GetPendingSize(1); p != 0 {
t.Errorf("expected 0 pending after compaction reset, got %d", p)
}
// Verify vid2size is the reported size, not a decayed value
vl.accessLock.RLock()
if vl.sizeTracking[1].effectiveSize != 2000 {
t.Errorf("expected vid2size=2000 after compaction, got %d", vl.sizeTracking[1].effectiveSize)
}
vl.accessLock.RUnlock()
}
func TestDrainAndRemoveFromWritable_NoPending(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":1000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// No pending — drain should return immediately
start := time.Now()
vl.DrainAndRemoveFromWritable(1)
if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
t.Errorf("drain with no pending took %v, expected near-instant", elapsed)
}
// Verify volume is no longer writable
writable, _ := vl.GetWritableVolumeCount()
if writable != 0 {
t.Errorf("expected 0 writable after drain, got %d", writable)
}
}
func TestDrainAndRemoveFromWritable_WithPending(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":1000, "replication":"000"},
{"id":2, "size":1000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// Add pending below threshold
vl.RecordAssign(1, int64(pendingSizeThreshold-1))
start := time.Now()
vl.DrainAndRemoveFromWritable(1)
elapsed := time.Since(start)
// Should return quickly since pending is below threshold
if elapsed > 500*time.Millisecond {
t.Errorf("drain with pending below threshold took %v", elapsed)
}
// Volume removed from writable
writables := vl.CloneWritableVolumes()
for _, vid := range writables {
if vid == 1 {
t.Error("volume 1 should not be writable after drain")
}
}
// Volume 2 still writable
found := false
for _, vid := range writables {
if vid == 2 {
found = true
}
}
if !found {
t.Error("volume 2 should still be writable")
}
}
func TestDrainAndRemoveFromWritable_DecaysViaConcurrentHeartbeat(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":1000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// Add large pending (well above threshold)
vl.RecordAssign(1, 100*1024*1024) // 100 MB
// Simulate heartbeats in background that will decay the pending.
// Advance lastUpdateTime before each call to bypass the 2s replica dedup.
done := make(chan struct{})
go func() {
defer close(done)
for i := 0; i < 10; i++ {
time.Sleep(500 * time.Millisecond)
vl.accessLock.Lock()
if st := vl.sizeTracking[1]; st != nil {
st.lastUpdateTime = time.Time{} // reset to allow update
}
vl.accessLock.Unlock()
vl.UpdateVolumeSize(1, 1000+uint64(i+1)*1000, 0, true)
}
}()
start := time.Now()
vl.DrainAndRemoveFromWritable(1)
elapsed := time.Since(start)
// Should have drained within a few seconds (heartbeats every 500ms).
// Use generous margin for slow CI.
if elapsed > 15*time.Second {
t.Errorf("drain took %v, expected faster with concurrent heartbeats", elapsed)
}
<-done
// Verify volume is no longer writable
writable, _ := vl.GetWritableVolumeCount()
if writable != 0 {
t.Errorf("expected 0 writable after drain, got %d", writable)
}
}
func TestSetVolumeReadOnly_PreservesPending(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":1000, "replication":"000"}
],
"limit":10
}
}
}
}
`
topo := setupWithLimit(t, layout, 10000)
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
dn := vl.Lookup(1)[0]
// Add some pending
vl.RecordAssign(1, 5000)
// SetVolumeReadOnly should succeed immediately (non-blocking)
result := vl.SetVolumeReadOnly(dn, 1)
if !result {
t.Error("expected SetVolumeReadOnly to return true")
}
// Pending is still there (not drained), but volume is readonly
writable, _ := vl.GetWritableVolumeCount()
if writable != 0 {
t.Errorf("expected 0 writable after readonly, got %d", writable)
}
if p := vl.GetPendingSize(1); p != 5000 {
t.Errorf("expected 5000 pending (not drained), got %d", p)
}
}

View File

@@ -0,0 +1,386 @@
package topology
import (
"fmt"
"testing"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
// Reproduces https://github.com/seaweedfs/seaweedfs/issues/8986
//
// Topology: 1 DC with 3 racks, replication "010" (different-rack).
// Each volume has replicas on 2 of the 3 racks.
// The third rack has no replica, so ShouldGrowVolumesByDcAndRack
// incorrectly returns true for that rack, causing endless volume creation
// even though the DC already has plenty of non-crowded writable volumes.
var topologyLayout3Racks = `
{
"dc_edge":{
"rack1":{
"server1":{
"ip":"10.0.0.1",
"volumes":[
{"id":1, "size":12312, "replication":"010"},
{"id":2, "size":12312, "replication":"010"}
],
"limit":30
}
},
"rack2":{
"server2":{
"ip":"10.0.0.2",
"volumes":[
{"id":1, "size":12312, "replication":"010"},
{"id":2, "size":12312, "replication":"010"}
],
"limit":30
}
},
"rack3":{
"server3":{
"ip":"10.0.0.3",
"volumes":[],
"limit":30
}
}
}
}
`
func TestShouldGrowVolumesByDcAndRack_Issue8986(t *testing.T) {
topo := setup(topologyLayout3Racks)
rp, _ := super_block.NewReplicaPlacementFromString("010")
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
writables := vl.CloneWritableVolumes()
if len(writables) != 2 {
t.Fatalf("expected 2 writable volumes, got %d", len(writables))
}
// rack1 and rack2 each have replicas of the writable volumes.
// ShouldGrowVolumesByDcAndRack should return false for them.
if vl.ShouldGrowVolumesByDcAndRack(&writables, "dc_edge", "rack1") {
t.Error("rack1 should NOT need growth — it has writable volumes")
}
if vl.ShouldGrowVolumesByDcAndRack(&writables, "dc_edge", "rack2") {
t.Error("rack2 should NOT need growth — it has writable volumes")
}
// rack3 has no replicas. With the old logic, this returns true (should grow),
// but since the DC already has non-crowded writable volumes with "010"
// replication, growing a new volume is unnecessary — the existing volumes
// can serve any write request that doesn't pin to a specific rack.
if vl.ShouldGrowVolumesByDcAndRack(&writables, "dc_edge", "rack3") {
t.Error("rack3 should NOT need growth — the DC already has non-crowded writable volumes that can serve writes")
}
}
// Topology from https://github.com/seaweedfs/seaweedfs/issues/9832
// 1 DC, 2 racks, 1 server each, replication 010, one crowded volume (28.65 GB)
// near the 30 GB size limit.
var topologyLayout9832 = `
{
"datacenter1":{
"rack1":{
"node-a":{
"ip":"10.0.0.1",
"volumes":[ {"id":12, "size":28650, "replication":"010", "collection":"bucket-nexus"} ],
"limit":30
}
},
"rack2":{
"node-b":{
"ip":"10.0.0.2",
"volumes":[ {"id":12, "size":28650, "replication":"010", "collection":"bucket-nexus"} ],
"limit":30
}
}
}
}
`
// Reproduces https://github.com/seaweedfs/seaweedfs/issues/9832
//
// A crowded "010" volume made the periodic rack-aware scan return true for
// every rack in the DC, so it grew once per rack with a hardcoded step of 2 —
// 2 racks × 2 = 4 logical volumes (8 physical) — and ignored a lowered
// master.volume_growth.copy_2. PlanRackAwareGrowth now plans a single DC-wide
// grow capped at copy_N.
func TestPlanRackAwareGrowth_Issue9832(t *testing.T) {
defer restoreCopyCounts(VolumeGrowStrategy.Copy1Count, VolumeGrowStrategy.Copy2Count)
VolumeGrowStrategy.Copy2Count = 1 // user's master.volume_growth.copy_2
topo := setupWithLimit(t, topologyLayout9832, 30000)
rp, _ := super_block.NewReplicaPlacementFromString("010")
vl := topo.GetVolumeLayout("bucket-nexus", rp, needle.EMPTY_TTL, types.HardDriveType)
if writables := vl.CloneWritableVolumes(); len(writables) != 1 {
t.Fatalf("expected 1 writable volume, got %d", len(writables))
}
// The crowded volume makes both racks report "should grow" — the source of
// the old per-rack multiplication.
writables := vl.CloneWritableVolumes()
if !vl.ShouldGrowVolumesByDcAndRack(&writables, "datacenter1", "rack1") ||
!vl.ShouldGrowVolumesByDcAndRack(&writables, "datacenter1", "rack2") {
t.Fatal("expected both racks to report should-grow for the crowded volume")
}
plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2)
if len(plans) != 1 {
t.Fatalf("expected 1 DC-wide grow, got %d: %+v", len(plans), plans)
}
if plans[0].Rack != "" {
t.Errorf("expected DC-wide grow (empty rack), got rack %q", plans[0].Rack)
}
if plans[0].WritableVolumeCount != 1 {
t.Errorf("expected copy_2=1 logical volume, got %d", plans[0].WritableVolumeCount)
}
}
// With the default copy_2 the per-event step is preserved (not increased): the
// fix only removes the per-rack multiplication.
func TestPlanRackAwareGrowth_DefaultStepNotMultiplied(t *testing.T) {
defer restoreCopyCounts(VolumeGrowStrategy.Copy1Count, VolumeGrowStrategy.Copy2Count)
VolumeGrowStrategy.Copy2Count = 6 // default
topo := setupWithLimit(t, topologyLayout9832, 30000)
rp, _ := super_block.NewReplicaPlacementFromString("010")
vl := topo.GetVolumeLayout("bucket-nexus", rp, needle.EMPTY_TTL, types.HardDriveType)
plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2)
if len(plans) != 1 {
t.Fatalf("expected 1 DC-wide grow, got %d: %+v", len(plans), plans)
}
if plans[0].WritableVolumeCount != 2 {
t.Errorf("expected step count 2 (min of step 2 and copy_2 6), got %d", plans[0].WritableVolumeCount)
}
}
// A non-crowded "010" volume needs no growth at all.
func TestPlanRackAwareGrowth_NotCrowdedNoGrowth(t *testing.T) {
layout := `
{
"datacenter1":{
"rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":12, "size":1000, "replication":"010", "collection":"c"} ], "limit":30 } },
"rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[ {"id":12, "size":1000, "replication":"010", "collection":"c"} ], "limit":30 } }
}
}
`
topo := setupWithLimit(t, layout, 30000)
rp, _ := super_block.NewReplicaPlacementFromString("010")
vl := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)
if plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2); len(plans) != 0 {
t.Fatalf("expected no growth for non-crowded volume, got %+v", plans)
}
}
// Non-rack-spanning replication ("000") still grows per rack: a rack without a
// writable volume gets its own grow.
func TestPlanRackAwareGrowth_PerRackForNonRackSpanning(t *testing.T) {
layout := `
{
"datacenter1":{
"rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":1000, "replication":"000", "collection":"c"} ], "limit":30 } },
"rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[], "limit":30 } }
}
}
`
topo := setupWithLimit(t, layout, 30000)
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)
plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2)
if len(plans) != 1 {
t.Fatalf("expected 1 per-rack grow, got %d: %+v", len(plans), plans)
}
if plans[0].Rack != "rack2" {
t.Errorf("expected grow pinned to empty rack2, got %q", plans[0].Rack)
}
}
// lastGrowCount is spread evenly across all grow targets even when DCs have
// different rack counts: each crowded "000" rack gets ceilDiv(lastGrowCount,
// totalRacks), so the total matches the request rather than over-growing per DC.
func TestPlanRackAwareGrowth_EvenDistributionAcrossUnevenDCs(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":28650, "replication":"000", "collection":"c"} ], "limit":30 } },
"rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[ {"id":2, "size":28650, "replication":"000", "collection":"c"} ], "limit":30 } }
},
"dc2":{
"rack3":{ "node-c":{ "ip":"10.0.0.3", "volumes":[ {"id":3, "size":28650, "replication":"000", "collection":"c"} ], "limit":30 } },
"rack4":{ "node-d":{ "ip":"10.0.0.4", "volumes":[ {"id":4, "size":28650, "replication":"000", "collection":"c"} ], "limit":30 } },
"rack5":{ "node-e":{ "ip":"10.0.0.5", "volumes":[ {"id":5, "size":28650, "replication":"000", "collection":"c"} ], "limit":30 } }
}
}
`
topo := setupWithLimit(t, layout, 30000)
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)
plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 10, 2)
if len(plans) != 5 {
t.Fatalf("expected a grow per crowded rack (5), got %d: %+v", len(plans), plans)
}
total := uint32(0)
for _, p := range plans {
if p.WritableVolumeCount != 2 { // ceilDiv(10, 5 racks)
t.Errorf("expected even per-rack count 2, got %d for %s/%s", p.WritableVolumeCount, p.DataCenter, p.Rack)
}
total += p.WritableVolumeCount
}
if total != 10 {
t.Errorf("expected total grow 10, got %d", total)
}
}
// A collection pinned to one data center (fs.configure -dataCenter) lives only
// there, yet the scan used to plan growth for every data center in the
// topology, spreading the collection's volumes across all of them. Data
// centers hosting none of the layout's volumes are not the scan's to fill.
func TestPlanRackAwareGrowth_SkipsDataCentersWithoutLayoutVolumes(t *testing.T) {
layout := `
{
"dc1":{ "rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":%d, "replication":"000", "collection":"pinned"} ], "limit":30 } } },
"dc2":{ "rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[], "limit":30 } } },
"dc3":{ "rack3":{ "node-c":{ "ip":"10.0.0.3", "volumes":[], "limit":30 } } },
"dc4":{ "rack4":{ "node-d":{ "ip":"10.0.0.4", "volumes":[], "limit":30 } } }
}
`
rp, _ := super_block.NewReplicaPlacementFromString("000")
topo := setupWithLimit(t, fmt.Sprintf(layout, 1000), 30000)
vl := topo.GetVolumeLayout("pinned", rp, needle.EMPTY_TTL, types.HardDriveType)
if plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2); len(plans) != 0 {
t.Fatalf("healthy dc1 volume: expected no growth, got %+v", plans)
}
topo = setupWithLimit(t, fmt.Sprintf(layout, 28650), 30000)
vl = topo.GetVolumeLayout("pinned", rp, needle.EMPTY_TTL, types.HardDriveType)
plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2)
if len(plans) != 1 {
t.Fatalf("crowded dc1 volume: expected 1 grow, got %d: %+v", len(plans), plans)
}
if plans[0].DataCenter != "dc1" || plans[0].Rack != "rack1" {
t.Errorf("expected grow pinned to dc1/rack1, got %s/%s", plans[0].DataCenter, plans[0].Rack)
}
}
// The lastGrowCount divisor counts only the racks the scan can actually plan
// for; racks of skipped data centers would dilute every grow.
func TestPlanRackAwareGrowth_DivisorCountsOnlyHostingRacks(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":28650, "replication":"000", "collection":"pinned"} ], "limit":30 } },
"rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[ {"id":2, "size":28650, "replication":"000", "collection":"pinned"} ], "limit":30 } }
},
"dc2":{
"rack3":{ "node-c":{ "ip":"10.0.0.3", "volumes":[], "limit":30 } },
"rack4":{ "node-d":{ "ip":"10.0.0.4", "volumes":[], "limit":30 } }
}
}
`
topo := setupWithLimit(t, layout, 30000)
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("pinned", rp, needle.EMPTY_TTL, types.HardDriveType)
plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 4, 2)
if len(plans) != 2 {
t.Fatalf("expected a grow per crowded dc1 rack, got %d: %+v", len(plans), plans)
}
for _, p := range plans {
if p.DataCenter != "dc1" {
t.Errorf("expected grows pinned to dc1, got %s/%s", p.DataCenter, p.Rack)
}
if p.WritableVolumeCount != 2 { // ceilDiv(4, 2 hosting racks), not ceilDiv(4, 4)
t.Errorf("expected per-rack count 2, got %d", p.WritableVolumeCount)
}
}
}
// The periodic must-grow and crowded growth paths build their request from
// ToVolumeGrowRequest. For a layout living in exactly one data center the
// request carries that DC, so those paths do not scatter a pinned collection
// either; a layout spanning DCs keeps unconstrained growth.
func TestToVolumeGrowRequest_SingleDataCenterLayout(t *testing.T) {
layout := `
{
"dc1":{ "rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":1000, "replication":"000", "collection":"pinned"} ], "limit":30 } } },
"dc2":{ "rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[ {"id":2, "size":1000, "replication":"000", "collection":"spread"} ], "limit":30 } } },
"dc3":{ "rack3":{ "node-c":{ "ip":"10.0.0.3", "volumes":[ {"id":3, "size":1000, "replication":"000", "collection":"spread"} ], "limit":30 } } }
}
`
topo := setupWithLimit(t, layout, 30000)
rp, _ := super_block.NewReplicaPlacementFromString("000")
pinned := &VolumeLayoutCollection{"pinned", topo.GetVolumeLayout("pinned", rp, needle.EMPTY_TTL, types.HardDriveType)}
if vgr := pinned.ToVolumeGrowRequest(); vgr.DataCenter != "dc1" {
t.Errorf("expected growth pinned to dc1, got %q", vgr.DataCenter)
}
spread := &VolumeLayoutCollection{"spread", topo.GetVolumeLayout("spread", rp, needle.EMPTY_TTL, types.HardDriveType)}
if vgr := spread.ToVolumeGrowRequest(); vgr.DataCenter != "" {
t.Errorf("expected unconstrained growth for a multi-DC layout, got %q", vgr.DataCenter)
}
}
// Cross-DC replication can never legitimately live in one data center: a
// single hosting DC there means the other DCs are down, and pinning growth to
// the survivor would encode the outage as a placement constraint.
func TestToVolumeGrowRequest_CrossDCReplicationNeverPinned(t *testing.T) {
layout := `
{
"dc1":{ "rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":1000, "replication":"100", "collection":"c"} ], "limit":30 } } },
"dc2":{ "rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[], "limit":30 } } }
}
`
topo := setupWithLimit(t, layout, 30000)
rp, _ := super_block.NewReplicaPlacementFromString("100")
vlc := &VolumeLayoutCollection{"c", topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)}
if vgr := vlc.ToVolumeGrowRequest(); vgr.DataCenter != "" {
t.Errorf("expected unconstrained growth for cross-DC replication, got %q", vgr.DataCenter)
}
}
// Volumes packed to capacity (e.g. by fs.mergeVolumes) go crowded and then
// unwritable, but stay in the crowded map. ShouldGrowVolumes must count only
// writable crowded volumes, or those leftovers keep writable <= crowded true
// forever and every assign-path grow request passes the gate.
func TestShouldGrowVolumes_UnwritableCrowdedVolumes(t *testing.T) {
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := NewVolumeLayout(rp, needle.EMPTY_TTL, types.HardDriveType, 30000, false)
vl.accessLock.Lock()
vl.setVolumeWritable(1)
vl.setVolumeWritable(2)
vl.accessLock.Unlock()
vl.SetVolumeCrowded(1)
vl.SetVolumeCapacityFull(1)
if _, crowded := vl.GetWritableVolumeCount(); crowded != 0 {
t.Fatalf("expected 0 writable crowded volumes, got %d", crowded)
}
if vl.ShouldGrowVolumes() {
t.Fatal("volume 2 still has room, growth is not needed")
}
vl.SetVolumeCrowded(2)
if !vl.ShouldGrowVolumes() {
t.Fatal("every writable volume is crowded, growth is needed")
}
}
func restoreCopyCounts(copy1, copy2 uint32) {
VolumeGrowStrategy.Copy1Count = copy1
VolumeGrowStrategy.Copy2Count = copy2
}

View File

@@ -0,0 +1,318 @@
package topology
import (
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
// A volume that grew past the limit keeps bouncing between writable and
// unwritable when the oversized mark is not refreshed on heartbeat: RegisterVolume
// sets it once, but the delta heartbeat path (ApplyVolumeChanges) never re-sets
// it, so ensureCorrectWritables sees a stale "not oversized" mark and re-adds
// the volume to writables on the next heartbeat, while RecordAssign removes it
// on the next assign. UpdateOversizedState must keep the mark current.
func TestUpdateOversizedStateKeepsOversizedVolumeUnwritable(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":4000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// Grow the volume past the limit, as RecordAssign would see it.
if !vl.RecordAssign(1, 9000) {
t.Fatalf("RecordAssign should report the volume reached capacity")
}
if w, _ := vl.GetWritableVolumeCount(); w != 0 {
t.Fatalf("expected 0 writable after RecordAssign, got %d", w)
}
// A heartbeat that reports the now-oversized volume must refresh the mark
// so ensureCorrectWritables does not re-add it to writables. Mirrors the
// heartbeat order: refresh, then decay, then correct.
dn := vl.vid2location[1].list[0]
vi, err := dn.GetVolumesById(1)
if err != nil {
t.Fatalf("GetVolumesById: %v", err)
}
oversized := vi
oversized.Size = 12000
vl.UpdateOversizedState(&oversized, dn)
vl.UpdateVolumeSize(1, oversized.Size, 0, true)
vl.EnsureCorrectWritables(&oversized)
if w, _ := vl.GetWritableVolumeCount(); w != 0 {
t.Fatalf("expected volume to stay unwritable after heartbeat refresh, got %d writable", w)
}
if !vl.vid2location[1].AnyOversized() {
t.Fatalf("expected AnyOversized to be true after heartbeat refresh")
}
}
// A volume that shrank back under the limit must have its oversized mark
// cleared by the heartbeat refresh, or it stays locked out of writables
// forever.
func TestUpdateOversizedStateClearsMarkWhenVolumeShrinks(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":4000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
dn := vl.vid2location[1].list[0]
vi, err := dn.GetVolumesById(1)
if err != nil {
t.Fatalf("GetVolumesById: %v", err)
}
// Mark oversized via a heartbeat that reports a huge size.
big := vi
big.Size = 12000
vl.UpdateOversizedState(&big, dn)
if !vl.vid2location[1].AnyOversized() {
t.Fatalf("expected AnyOversized after a huge report")
}
// A later heartbeat with a normal size clears the mark.
vl.UpdateOversizedState(&vi, dn)
if vl.vid2location[1].AnyOversized() {
t.Fatalf("expected AnyOversized cleared after the volume shrank")
}
}
// A volume removed for capacity must not be re-added to writables by
// ensureCorrectWritables before capacityRecoveryDelay elapses, even after its
// oversized mark is cleared by a heartbeat that reports a smaller size.
func TestEnsureCorrectWritablesHonorsRecoveryCooldown(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":4000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// Remove the volume for capacity, as RecordAssign would.
if !vl.RecordAssign(1, 9000) {
t.Fatalf("RecordAssign should report the volume reached capacity")
}
if w, _ := vl.GetWritableVolumeCount(); w != 0 {
t.Fatalf("expected 0 writable after RecordAssign, got %d", w)
}
dn := vl.vid2location[1].list[0]
vi, err := dn.GetVolumesById(1)
if err != nil {
t.Fatalf("GetVolumesById: %v", err)
}
// Heartbeat reports the volume shrank back under the limit: the oversized
// mark clears and effectiveSize decays, but the volume is still within
// capacityRecoveryDelay.
vi.Size = 4000
vl.UpdateOversizedState(&vi, dn)
vl.UpdateVolumeSize(1, vi.Size, 0, true)
vl.EnsureCorrectWritables(&vi)
if w, _ := vl.GetWritableVolumeCount(); w != 0 {
t.Fatalf("expected volume to stay unwritable during the cooldown, got %d writable", w)
}
// After the cooldown, a heartbeat with the shrunken size recovers it.
advanceSizeTrackingClock(vl, 1, capacityRecoveryDelay+time.Second)
if !vl.UpdateVolumeSize(1, vi.Size, 0, true) {
t.Fatalf("expected volume to recover to writable after the cooldown")
}
vl.EnsureCorrectWritables(&vi)
if w, _ := vl.GetWritableVolumeCount(); w != 1 {
t.Fatalf("expected volume writable after the cooldown, got %d", w)
}
}
// After the cooldown elapses, a volume whose effective size is still at the
// limit must not be restored by ensureCorrectWritables: the next assign would
// remove it again.
func TestEnsureCorrectWritablesDoesNotRestoreVolumeStillAtLimit(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":8000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// Remove the volume for capacity, as RecordAssign would. effectiveSize
// lands at 13000, past the limit (10000); after the decay against a
// reported size of 8000 it stays at 10500, still past the limit.
if !vl.RecordAssign(1, 5000) {
t.Fatalf("RecordAssign should report the volume reached capacity")
}
if w, _ := vl.GetWritableVolumeCount(); w != 0 {
t.Fatalf("expected 0 writable after RecordAssign, got %d", w)
}
dn := vl.vid2location[1].list[0]
vi, err := dn.GetVolumesById(1)
if err != nil {
t.Fatalf("GetVolumesById: %v", err)
}
// Heartbeat reports a size below the limit: the oversized mark clears,
// but the decay only halves the pending estimate, so effectiveSize stays
// past the limit and UpdateVolumeSize refuses recovery.
vi.Size = 8000
vl.UpdateOversizedState(&vi, dn)
vl.UpdateVolumeSize(1, vi.Size, 0, true)
if vl.vid2location[1].AnyOversized() {
t.Fatalf("expected oversized mark cleared after the shrink report")
}
// After the cooldown, the volume is still at the limit:
// ensureCorrectWritables must not override UpdateVolumeSize's refusal.
advanceSizeTrackingClock(vl, 1, capacityRecoveryDelay+time.Second)
vl.EnsureCorrectWritables(&vi)
if w, _ := vl.GetWritableVolumeCount(); w != 0 {
t.Fatalf("expected volume at the limit to stay unwritable, got %d writable", w)
}
}
// A crowded volume is not a full one: it is above the growth threshold but
// still has room. One that drops out of writables while a replica is away must
// come back when the replica returns -- nothing writes to a volume that is not
// writable, so its size can never fall on its own and the lockout would be
// permanent.
func TestEnsureCorrectWritablesRestoresCrowdedVolumeAfterReplicaReturns(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"ip":"10.0.0.1",
"volumes":[
{"id":1, "size":9500, "replication":"001"}
],
"limit":10
},
"server2":{
"ip":"10.0.0.2",
"volumes":[
{"id":1, "size":9500, "replication":"001"}
],
"limit":10
}
}
}
}
`
topo := setupWithLimit(t, layout, 10000)
rp, _ := super_block.NewReplicaPlacementFromString("001")
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
// 9500 is past the growth threshold (9000) but under the limit (10000).
vl.UpdateVolumeSize(1, 9500, 0, true)
if _, crowded := vl.crowded[1]; !crowded {
t.Fatalf("expected the volume to be crowded")
}
if w, _ := vl.GetWritableVolumeCount(); w != 1 {
t.Fatalf("a crowded volume is still writable, got %d writable", w)
}
dn := vl.vid2location[1].list[1]
vi, err := dn.GetVolumesById(1)
if err != nil {
t.Fatalf("GetVolumesById: %v", err)
}
vl.UnRegisterVolume(&vi, dn)
if w, _ := vl.GetWritableVolumeCount(); w != 0 {
t.Fatalf("expected 0 writable while a replica is missing, got %d", w)
}
// The replica comes back on the next heartbeat.
topo.RegisterVolumeLayout(vi, dn)
advanceSizeTrackingClock(vl, 1, 5*time.Second)
vl.UpdateOversizedState(&vi, dn)
vl.UpdateVolumeSize(1, vi.Size, 0, true)
vl.EnsureCorrectWritables(&vi)
if w, _ := vl.GetWritableVolumeCount(); w != 1 {
t.Fatalf("expected the volume writable again, got %d", w)
}
}
// An incremental heartbeat announces an arrival with a short message that
// carries no size. Registering from it must not clear the oversized mark the
// full heartbeat set, or the volume is handed back to the writable list until
// the next full report.
func TestIncrementalRegistrationKeepsOversizedMark(t *testing.T) {
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), 32*1024, 5, false)
dc := topo.GetOrCreateDataCenter("dc1")
rack := dc.GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("127.0.0.1", 34534, 0, "127.0.0.1", "", map[string]uint32{"": 25})
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
// A full heartbeat reports the volume past the 32 KB limit.
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{{
Id: 1,
Size: uint64(64 * 1024),
ReplicaPlacement: uint32(0),
Version: uint32(needle.GetCurrentVersion()),
}}, dn)
if w, _ := vl.GetWritableVolumeCount(); w != 0 {
t.Fatalf("expected the oversized volume out of writables, got %d", w)
}
// The same volume is announced again as an arrival.
topo.IncrementalSyncDataNodeRegistration([]*master_pb.VolumeShortInformationMessage{{
Id: 1,
ReplicaPlacement: uint32(0),
Version: uint32(needle.GetCurrentVersion()),
}}, nil, dn)
if !vl.vid2location[1].AnyOversized() {
t.Fatalf("expected the oversized mark to survive the arrival announcement")
}
if w, _ := vl.GetWritableVolumeCount(); w != 0 {
t.Fatalf("expected the oversized volume to stay out of writables, got %d", w)
}
}

View File

@@ -0,0 +1,134 @@
package topology
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
var benchLayoutSmall = `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":2000, "replication":"000"},
{"id":2, "size":5000, "replication":"000"},
{"id":3, "size":8000, "replication":"000"}
],
"limit":10
}
}
}
}
`
var benchLayoutMedium = `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":1000, "replication":"000"},
{"id":2, "size":2000, "replication":"000"},
{"id":3, "size":3000, "replication":"000"},
{"id":4, "size":4000, "replication":"000"},
{"id":5, "size":5000, "replication":"000"},
{"id":6, "size":6000, "replication":"000"},
{"id":7, "size":7000, "replication":"000"},
{"id":8, "size":8000, "replication":"000"},
{"id":9, "size":9000, "replication":"000"},
{"id":10, "size":9500, "replication":"000"}
],
"limit":20
}
}
}
}
`
var benchLayoutLarge = `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":500, "replication":"000"},
{"id":2, "size":1000, "replication":"000"},
{"id":3, "size":1500, "replication":"000"},
{"id":4, "size":2000, "replication":"000"},
{"id":5, "size":2500, "replication":"000"},
{"id":6, "size":3000, "replication":"000"},
{"id":7, "size":3500, "replication":"000"},
{"id":8, "size":4000, "replication":"000"},
{"id":9, "size":4500, "replication":"000"},
{"id":10, "size":5000, "replication":"000"},
{"id":11, "size":5500, "replication":"000"},
{"id":12, "size":6000, "replication":"000"},
{"id":13, "size":6500, "replication":"000"},
{"id":14, "size":7000, "replication":"000"},
{"id":15, "size":7500, "replication":"000"},
{"id":16, "size":8000, "replication":"000"},
{"id":17, "size":8500, "replication":"000"},
{"id":18, "size":9000, "replication":"000"},
{"id":19, "size":9200, "replication":"000"},
{"id":20, "size":9500, "replication":"000"}
],
"limit":30
}
}
}
}
`
func benchPickForWrite(b *testing.B, layout string, volumeSizeLimit uint64) {
topo := setupWithLimit(b, layout, volumeSizeLimit)
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
option := &VolumeGrowOption{}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
vl.PickForWrite(1, option)
}
}
func BenchmarkPickForWrite_3Volumes(b *testing.B) {
benchPickForWrite(b, benchLayoutSmall, 10000)
}
func BenchmarkPickForWrite_10Volumes(b *testing.B) {
benchPickForWrite(b, benchLayoutMedium, 10000)
}
func BenchmarkPickForWrite_20Volumes(b *testing.B) {
benchPickForWrite(b, benchLayoutLarge, 10000)
}
func benchPickForWriteConstrained(b *testing.B, layout string, volumeSizeLimit uint64) {
topo := setupWithLimit(b, layout, volumeSizeLimit)
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
option := &VolumeGrowOption{DataCenter: "dc1"}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
vl.PickForWrite(1, option)
}
}
func BenchmarkPickForWriteConstrained_3Volumes(b *testing.B) {
benchPickForWriteConstrained(b, benchLayoutSmall, 10000)
}
func BenchmarkPickForWriteConstrained_10Volumes(b *testing.B) {
benchPickForWriteConstrained(b, benchLayoutMedium, 10000)
}
func BenchmarkPickForWriteConstrained_20Volumes(b *testing.B) {
benchPickForWriteConstrained(b, benchLayoutLarge, 10000)
}

View File

@@ -0,0 +1,786 @@
package topology
import (
"encoding/json"
"math"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
// setupWithLimit is like setup() but allows specifying the volumeSizeLimit
// so that VolumeLayouts are created with the correct limit from the start.
func setupWithLimit(t testing.TB, topologyLayout string, volumeSizeLimit uint64) *Topology {
t.Helper()
var data interface{}
if err := json.Unmarshal([]byte(topologyLayout), &data); err != nil {
t.Fatalf("setupWithLimit: json.Unmarshal: %v", err)
}
mTopology, ok := data.(map[string]interface{})
if !ok {
t.Fatalf("setupWithLimit: expected map[string]interface{}, got %T", data)
}
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), volumeSizeLimit, 5, false)
for dcKey, dcValue := range mTopology {
dc := NewDataCenter(dcKey)
dcMap := dcValue.(map[string]interface{})
topo.LinkChildNode(dc)
for rackKey, rackValue := range dcMap {
dcRack := NewRack(rackKey)
rackMap := rackValue.(map[string]interface{})
dc.LinkChildNode(dcRack)
for serverKey, serverValue := range rackMap {
server := NewDataNode(serverKey)
serverMap := serverValue.(map[string]interface{})
if ip, ok := serverMap["ip"]; ok {
server.Ip = ip.(string)
}
dcRack.LinkChildNode(server)
for _, v := range serverMap["volumes"].([]interface{}) {
m := v.(map[string]interface{})
vi := storage.VolumeInfo{
Id: needle.VolumeId(int64(m["id"].(float64))),
Size: uint64(m["size"].(float64)),
Version: needle.GetCurrentVersion(),
}
if mVal, ok := m["fileCount"]; ok {
vi.FileCount = uint32(mVal.(float64))
}
if mVal, ok := m["collection"]; ok {
vi.Collection = mVal.(string)
}
if mVal, ok := m["replication"]; ok {
rp, _ := super_block.NewReplicaPlacementFromString(mVal.(string))
vi.ReplicaPlacement = rp
}
if vi.ReplicaPlacement != nil {
vl := topo.GetVolumeLayout(vi.Collection, vi.ReplicaPlacement, needle.EMPTY_TTL, types.HardDriveType)
vl.RegisterVolume(&vi, server)
vl.setVolumeWritable(vi.Id)
}
server.AddOrUpdateVolume(vi)
}
disk := server.getOrCreateDisk("")
disk.UpAdjustDiskUsageDelta("", &DiskUsageCounts{
maxVolumeCount: int64(serverMap["limit"].(float64)),
})
}
}
}
return topo
}
func setupPickTest(t testing.TB, layout string, volumeSizeLimit uint64) (*Topology, *VolumeLayout) {
t.Helper()
topo := setupWithLimit(t, layout, volumeSizeLimit)
rp, _ := super_block.NewReplicaPlacementFromString("000")
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
return topo, vl
}
func TestPickForWriteWeightedDistribution(t *testing.T) {
// 3 volumes at 20%, 50%, 80% full (sizes 2000, 5000, 8000 of limit 10000)
// remaining: 8000, 5000, 2000 => ratios ~53%, 33%, 13%
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":2000, "replication":"000"},
{"id":2, "size":5000, "replication":"000"},
{"id":3, "size":8000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
counts := make(map[needle.VolumeId]int)
option := &VolumeGrowOption{}
n := 60000
for i := 0; i < n; i++ {
vid, _, _, _, err := vl.PickForWrite(1, option)
if err != nil {
t.Fatalf("PickForWrite: %v", err)
}
counts[vid]++
}
// vid 1 (remaining 8000) > vid 2 (remaining 5000) > vid 3 (remaining 2000)
if counts[1] <= counts[3] {
t.Errorf("expected vid 1 picked more than vid 3: vid1=%d, vid3=%d", counts[1], counts[3])
}
if counts[2] <= counts[3] {
t.Errorf("expected vid 2 picked more than vid 3: vid2=%d, vid3=%d", counts[2], counts[3])
}
// Check proportions: expected 8000/5000/2000 out of 15000
expected := map[needle.VolumeId]float64{
1: 8000.0 / 15000.0,
2: 5000.0 / 15000.0,
3: 2000.0 / 15000.0,
}
for vid, expectedPct := range expected {
actualPct := float64(counts[vid]) / float64(n)
if math.Abs(actualPct-expectedPct) > 0.03 {
t.Errorf("vid %d: expected ~%.1f%%, got %.1f%%", vid, expectedPct*100, actualPct*100)
}
}
}
func TestPickForWriteWithPendingSize(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":1000, "replication":"000"},
{"id":2, "size":1000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// Add large pending to vid 1, making it effectively 9000/10000
vl.RecordAssign(1, 8000)
counts := make(map[needle.VolumeId]int)
option := &VolumeGrowOption{}
n := 10000
for i := 0; i < n; i++ {
vid, _, _, _, err := vl.PickForWrite(1, option)
if err != nil {
t.Fatalf("PickForWrite: %v", err)
}
counts[vid]++
}
// vid 2 (remaining ~9000) should be picked much more than vid 1 (remaining ~1000)
ratio := float64(counts[2]) / float64(counts[1])
if ratio < 3.0 {
t.Errorf("vid2/vid1 ratio %.2f expected >= 3.0 (vid1=%d, vid2=%d)", ratio, counts[1], counts[2])
}
}
// A flat 1MB charge per hintless file id overcharges a small-file workload by
// orders of magnitude, marking volumes full while they hold a fraction of the
// limit.
func TestPickForWriteEstimatesPendingSizeFromVolumeAverage(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":409600, "fileCount":100, "replication":"000"}
],
"limit":10
}
}
}
}
`
topo, vl := setupPickTest(t, layout, 30*1024*1024)
rp, _ := super_block.NewReplicaPlacementFromString("000")
option := &VolumeGrowOption{ReplicaPlacement: rp}
pendingAfterAssign := func(expectedDataSize uint64) uint64 {
vl.accessLock.RLock()
before := vl.sizeTracking[1].effectiveSize
vl.accessLock.RUnlock()
if _, _, _, _, err := topo.PickForWrite(1, option, vl, expectedDataSize); err != nil {
t.Fatalf("PickForWrite: %v", err)
}
vl.accessLock.RLock()
defer vl.accessLock.RUnlock()
return vl.sizeTracking[1].effectiveSize - before
}
if got := pendingAfterAssign(0); got != 4096 {
t.Errorf("a hintless assign charged %d, want the volume's 4096-byte average", got)
}
if got := pendingAfterAssign(8192); got != 8192 {
t.Errorf("a hinted assign charged %d, want the 8192-byte hint to win over the average", got)
}
}
// A volume with no files yet has no average to draw on, so the 1MB fallback
// still applies.
func TestPickForWriteEstimateFallsBackWithoutHistory(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":0, "replication":"000"}
],
"limit":10
}
}
}
}
`
topo, vl := setupPickTest(t, layout, 30*1024*1024)
rp, _ := super_block.NewReplicaPlacementFromString("000")
option := &VolumeGrowOption{ReplicaPlacement: rp}
if _, _, _, _, err := topo.PickForWrite(1, option, vl, 0); err != nil {
t.Fatalf("PickForWrite: %v", err)
}
vl.accessLock.RLock()
pending := vl.sizeTracking[1].effectiveSize - vl.sizeTracking[1].reportedSize
vl.accessLock.RUnlock()
if pending != DefaultNeedleSizeEstimate {
t.Errorf("a hintless assign on an empty volume charged %d, want the %d fallback", pending, DefaultNeedleSizeEstimate)
}
}
func TestPickForWriteSingleWritable(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":5000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
option := &VolumeGrowOption{}
for i := 0; i < 100; i++ {
vid, _, _, _, err := vl.PickForWrite(1, option)
if err != nil {
t.Fatalf("PickForWrite: %v", err)
}
if vid != 1 {
t.Fatalf("expected vid 1, got %d", vid)
}
}
}
func TestPickForWriteAllNearFull(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":9999, "replication":"000"},
{"id":2, "size":9999, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
option := &VolumeGrowOption{}
for i := 0; i < 100; i++ {
vid, _, _, _, err := vl.PickForWrite(1, option)
if err != nil {
t.Fatalf("PickForWrite: %v", err)
}
if vid != 1 && vid != 2 {
t.Fatalf("expected vid 1 or 2, got %d", vid)
}
}
}
func TestPickForWriteConstrainedWeighted(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"ip":"10.0.0.1",
"volumes":[
{"id":1, "size":2000, "replication":"000"},
{"id":2, "size":8000, "replication":"000"}
],
"limit":10
}
},
"rack2":{
"server2":{
"ip":"10.0.0.2",
"volumes":[
{"id":3, "size":5000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
counts := make(map[needle.VolumeId]int)
option := &VolumeGrowOption{DataCenter: "dc1"}
n := 20000
for i := 0; i < n; i++ {
vid, _, _, _, err := vl.PickForWrite(1, option)
if err != nil {
t.Fatalf("PickForWrite: %v", err)
}
counts[vid]++
}
// vid 1 (remaining 8000) should be picked most, vid 2 (remaining 2000) least
if counts[1] <= counts[2] {
t.Errorf("expected vid 1 picked more than vid 2: vid1=%d, vid2=%d", counts[1], counts[2])
}
}
func TestRecordAssignMarksCrowded(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":8500, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// Volume at 85% — not crowded yet (threshold is 90%)
_, crowded := vl.GetWritableVolumeCount()
if crowded != 0 {
t.Fatalf("expected 0 crowded, got %d", crowded)
}
// Add pending that pushes past 90%
vl.RecordAssign(1, 1000)
_, crowded = vl.GetWritableVolumeCount()
if crowded != 1 {
t.Errorf("expected 1 crowded after pending push past 90%%, got %d", crowded)
}
}
func TestRecordAssignReachingCapacityRemovesFromWritable(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":5000, "replication":"000"},
{"id":2, "size":5000, "replication":"000"}
],
"limit":10
}
}
}
}
`
topo, vl := setupPickTest(t, layout, 10000)
writable, _ := vl.GetWritableVolumeCount()
if writable != 2 {
t.Fatalf("expected 2 writable volumes initially, got %d", writable)
}
// Each volume counts as active initially.
initialActive := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount
// Push vid 1 past the hard limit (5000 + 5000 = 10000 == limit).
reachedCapacity := vl.RecordAssign(1, 5000)
if !reachedCapacity {
t.Fatalf("RecordAssign should return true when effectiveSize reaches limit")
}
vl.AdjustActiveVolumeCountForFull(1)
writable, _ = vl.GetWritableVolumeCount()
if writable != 1 {
t.Errorf("expected 1 writable after eager removal, got %d", writable)
}
// activeVolumeCount should be decremented for the data node holding vid 1.
afterActive := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount
if afterActive != initialActive-1 {
t.Errorf("expected activeVolumeCount=%d, got %d", initialActive-1, afterActive)
}
// A second RecordAssign on the already-removed volume should not return
// true again (no double accounting).
if vl.RecordAssign(1, 10000) {
t.Errorf("RecordAssign should not report reachedCapacity twice for the same removal")
}
afterSecond := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount
if afterSecond != afterActive {
t.Errorf("activeVolumeCount changed on second RecordAssign: before=%d after=%d", afterActive, afterSecond)
}
}
// advanceSizeTrackingClock backdates a volume's time-sensitive fields by d
// so heartbeat decay and the recovery delay fire on the next update.
func advanceSizeTrackingClock(vl *VolumeLayout, vid needle.VolumeId, d time.Duration) {
vl.accessLock.Lock()
defer vl.accessLock.Unlock()
st := vl.sizeTracking[vid]
if st == nil {
return
}
if !st.lastUpdateTime.IsZero() {
st.lastUpdateTime = st.lastUpdateTime.Add(-d)
}
if !st.fullSince.IsZero() {
st.fullSince = st.fullSince.Add(-d)
}
}
func TestUpdateVolumeSizeRecoversEagerlyRemovedVolume(t *testing.T) {
// Two writable volumes, each at 40% of a 10000-byte limit. Push vid 1
// past the hard limit via RecordAssign, then heartbeat with an
// unchanged reported size so decay shrinks effectiveSize below the
// crowded threshold (90% of limit). After the recovery delay, the
// volume should be re-added to writables and activeVolumeCount
// restored.
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":4000, "replication":"000"},
{"id":2, "size":4000, "replication":"000"}
],
"limit":10
}
}
}
}
`
topo, vl := setupPickTest(t, layout, 10000)
initialActive := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount
initialWritables, _ := vl.GetWritableVolumeCount()
// Push vid 1 past the limit (effective = 4000 + 6000 = 10000).
if !vl.RecordAssign(1, 6000) {
t.Fatalf("RecordAssign should return true at the limit")
}
vl.AdjustActiveVolumeCountForFull(1)
w, _ := vl.GetWritableVolumeCount()
if w != initialWritables-1 {
t.Fatalf("expected %d writables after eager removal, got %d", initialWritables-1, w)
}
// Before the recovery delay, a heartbeat that lets decay run should
// *not* re-add the volume (even though effectiveSize would now be
// under the threshold).
advanceSizeTrackingClock(vl, 1, 3*time.Second) // past the 2s dedup window, but before 30s delay
if vl.UpdateVolumeSize(1, 4000, 0, true) {
t.Fatalf("recovery should not fire before capacityRecoveryDelay")
}
w, _ = vl.GetWritableVolumeCount()
if w != initialWritables-1 {
t.Errorf("writable count should not change before delay, got %d", w)
}
// Now skip past the recovery delay. Decay the gap further until
// effectiveSize drops below the crowded threshold (9000).
for i := 0; i < 6; i++ {
advanceSizeTrackingClock(vl, 1, 10*time.Second)
recovered := vl.UpdateVolumeSize(1, 4000, 0, true)
if recovered {
vl.AdjustActiveVolumeCountAfterRecovery(1)
break
}
}
w, _ = vl.GetWritableVolumeCount()
if w != initialWritables {
t.Errorf("expected recovery to restore %d writables, got %d", initialWritables, w)
}
if got := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount; got != initialActive {
t.Errorf("expected activeVolumeCount restored to %d, got %d", initialActive, got)
}
// fullSince should have been cleared so a subsequent heartbeat doesn't
// try to recover again.
advanceSizeTrackingClock(vl, 1, 60*time.Second)
if vl.UpdateVolumeSize(1, 4000, 0, true) {
t.Errorf("recovery should not re-fire after the volume is already writable")
}
}
func TestUpdateVolumeSizeNoRecoveryWhenDiskStillOversized(t *testing.T) {
// Volume reported at 95% of limit and pushed past limit by RecordAssign.
// Even after the recovery delay and decay, reportedSize remains >= limit
// (real on-disk size is over limit), so recovery must not fire.
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":9500, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
if !vl.RecordAssign(1, 500) {
t.Fatalf("RecordAssign should hit the limit (9500 + 500 = 10000)")
}
vl.AdjustActiveVolumeCountForFull(1)
// Plenty of time elapsed — but reported stays at 10500 (over limit).
for i := 0; i < 5; i++ {
advanceSizeTrackingClock(vl, 1, 10*time.Second)
if vl.UpdateVolumeSize(1, 10500, 0, true) {
t.Fatalf("recovery must not fire when reported >= limit")
}
}
w, _ := vl.GetWritableVolumeCount()
if w != 0 {
t.Errorf("expected 0 writables (volume legitimately full), got %d", w)
}
}
func TestHeartbeatDecaysPendingSize(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":1000, "replication":"000"},
{"id":2, "size":1000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// vid2size starts at 1000 (reported). Add 8000 pending → 9000.
vl.RecordAssign(1, 8000)
vl.accessLock.RLock()
if vl.sizeTracking[1].effectiveSize != 9000 {
t.Fatalf("expected vid2size=9000 after RecordAssign, got %d", vl.sizeTracking[1].effectiveSize)
}
vl.accessLock.RUnlock()
// Helper to simulate a new heartbeat cycle (advance past dedup window)
advanceCycle := func() {
vl.accessLock.Lock()
vl.sizeTracking[1].lastUpdateTime = time.Now().Add(-3 * time.Second)
vl.accessLock.Unlock()
}
// Heartbeat: volume server reports size=3000 (some writes landed).
// Old effective=9000, new reported=3000 → excess=6000 → decayed to 3000.
// So vid2size should become 3000 + 6000/2 = 6000, not just 3000.
vl.UpdateVolumeSize(1, 3000, 0, true)
vl.accessLock.RLock()
if vl.sizeTracking[1].effectiveSize != 6000 {
t.Errorf("expected vid2size=6000 after decay (3000 + 6000/2), got %d", vl.sizeTracking[1].effectiveSize)
}
vl.accessLock.RUnlock()
// Second heartbeat: size=5000. Old effective=6000 → excess=1000 → decay to 500.
// vid2size should become 5000 + 1000/2 = 5500.
advanceCycle()
vl.UpdateVolumeSize(1, 5000, 0, true)
vl.accessLock.RLock()
if vl.sizeTracking[1].effectiveSize != 5500 {
t.Errorf("expected vid2size=5500 after second decay (5000 + 1000/2), got %d", vl.sizeTracking[1].effectiveSize)
}
vl.accessLock.RUnlock()
// Third heartbeat: size=5500. Old effective=5500 → no excess.
// vid2size should be exactly 5500.
advanceCycle()
vl.UpdateVolumeSize(1, 5500, 0, true)
vl.accessLock.RLock()
if vl.sizeTracking[1].effectiveSize != 5500 {
t.Errorf("expected vid2size=5500 (no excess), got %d", vl.sizeTracking[1].effectiveSize)
}
vl.accessLock.RUnlock()
// vid 2 (remaining 9000) should be picked more than vid 1 (remaining 4500)
counts := make(map[needle.VolumeId]int)
option := &VolumeGrowOption{}
for i := 0; i < 10000; i++ {
vid, _, _, _, err := vl.PickForWrite(1, option)
if err != nil {
t.Fatalf("PickForWrite: %v", err)
}
counts[vid]++
}
if counts[2] <= counts[1] {
t.Errorf("vid 2 (remaining 9000) should be picked more than vid 1 (remaining 4500): vid1=%d, vid2=%d", counts[1], counts[2])
}
}
func TestHeartbeatDecayDedupReplicas(t *testing.T) {
// Volume 1 replicated on server1 and server2.
// Both servers report size=3000 in the same heartbeat cycle.
// Decay should run only once, not once per replica.
layout := `
{
"dc1":{
"rack1":{
"server1":{
"ip":"10.0.0.1",
"volumes":[
{"id":1, "size":1000, "replication":"001"}
],
"limit":10
},
"server2":{
"ip":"10.0.0.2",
"volumes":[
{"id":1, "size":1000, "replication":"001"}
],
"limit":10
}
}
}
}
`
topo := setupWithLimit(t, layout, 10000)
rp, _ := super_block.NewReplicaPlacementFromString("001")
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
// Add pending: effective = 1000 + 8000 = 9000
vl.RecordAssign(1, 8000)
vl.accessLock.RLock()
if vl.sizeTracking[1].effectiveSize != 9000 {
t.Fatalf("expected vid2size=9000, got %d", vl.sizeTracking[1].effectiveSize)
}
vl.accessLock.RUnlock()
// Both replicas report size=3000. Decay should happen once: 3000 + (9000-3000)/2 = 6000.
// Calling UpdateVolumeSize twice simulates two replicas reporting in the same cycle.
vl.UpdateVolumeSize(1, 3000, 0, true)
vl.UpdateVolumeSize(1, 3000, 0, true) // second replica, same size — should be a no-op
vl.accessLock.RLock()
got := vl.sizeTracking[1].effectiveSize
vl.accessLock.RUnlock()
// Without dedup: would be 3000 + (6000-3000)/2 = 4500 (double decay).
// With dedup: should be 6000 (single decay).
if got != 6000 {
t.Errorf("expected vid2size=6000 (single decay), got %d (double decay would give 4500)", got)
}
}
func TestUpdateVolumeSize_DecaysEvenWhenReportedSizeUnchanged(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"volumes":[
{"id":1, "size":1000, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
// Add pending: effective = 1000 + 8000 = 9000
vl.RecordAssign(1, 8000)
if p := vl.GetPendingSize(1); p != 8000 {
t.Fatalf("expected 8000 pending, got %d", p)
}
// First heartbeat: reported size unchanged at 1000 (writes haven't landed).
// Decay should still run: 1000 + (9000-1000)/2 = 5000.
vl.UpdateVolumeSize(1, 1000, 0, true)
if p := vl.GetPendingSize(1); p != 4000 {
t.Errorf("expected 4000 pending after first decay, got %d", p)
}
// Simulate next heartbeat cycle (>2s later) with same reported size.
// Need to advance lastUpdateTime — manipulate directly under lock.
vl.accessLock.Lock()
vl.sizeTracking[1].lastUpdateTime = time.Now().Add(-3 * time.Second)
vl.accessLock.Unlock()
// Second heartbeat: still 1000. Decay again: 1000 + (5000-1000)/2 = 3000.
vl.UpdateVolumeSize(1, 1000, 0, true)
if p := vl.GetPendingSize(1); p != 2000 {
t.Errorf("expected 2000 pending after second decay, got %d", p)
}
}
func TestShouldGrowVolumesByDcAndRack_WithPendingSize(t *testing.T) {
layout := `
{
"dc1":{
"rack1":{
"server1":{
"ip":"10.0.0.1",
"volumes":[
{"id":1, "size":8500, "replication":"000"}
],
"limit":10
}
}
}
}
`
_, vl := setupPickTest(t, layout, 10000)
writables := vl.CloneWritableVolumes()
if vl.ShouldGrowVolumesByDcAndRack(&writables, "dc1", "rack1") {
t.Error("should not grow before pending makes volume crowded")
}
// Add pending that pushes effective size past 9000 threshold
vl.RecordAssign(1, 600)
if !vl.ShouldGrowVolumesByDcAndRack(&writables, "dc1", "rack1") {
t.Error("should grow after pending pushes volume past crowded threshold")
}
}

View File

@@ -0,0 +1,163 @@
package topology
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
func sizeTrackingLayout(t *testing.T) (*Topology, *DataNode, *VolumeLayout) {
t.Helper()
topo := NewTopology("st", nil, 32*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("127.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 100})
rp, _ := super_block.NewReplicaPlacementFromString("000")
return topo, dn, topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)
}
func sizeTrackingVolume(id uint32, size uint64, readOnly bool) *master_pb.VolumeInformationMessage {
return &master_pb.VolumeInformationMessage{
Id: id, Size: size, Collection: "c", Version: 3, ReadOnly: readOnly,
}
}
func TestSizeTrackingSkipsReadOnlyVolumes(t *testing.T) {
topo, dn, vl := sizeTrackingLayout(t)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
sizeTrackingVolume(1, 1000, false), sizeTrackingVolume(2, 1000, true),
}, dn)
vl.accessLock.RLock()
_, writableTracked := vl.sizeTracking[needle.VolumeId(1)]
_, readOnlyTracked := vl.sizeTracking[needle.VolumeId(2)]
vl.accessLock.RUnlock()
if !writableTracked {
t.Error("a writable volume lost its size tracking, so assignments stop being accounted for")
}
if readOnlyTracked {
t.Error("a read-only volume is tracked for writes it can never take")
}
}
// A volume that goes read-only after the master already tracked it has to give
// the entry back, or a cluster that tiers its volumes keeps paying for them.
func TestSizeTrackingReleasedWhenAVolumeGoesReadOnly(t *testing.T) {
topo, dn, vl := sizeTrackingLayout(t)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{sizeTrackingVolume(1, 1000, false)}, dn)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{sizeTrackingVolume(1, 1000, true)}, dn)
vl.accessLock.RLock()
_, tracked := vl.sizeTracking[needle.VolumeId(1)]
vl.accessLock.RUnlock()
if tracked {
t.Error("a volume that became read-only kept its size tracking")
}
}
func TestSizeTrackingReturnsWhenAVolumeBecomesWritable(t *testing.T) {
topo, dn, vl := sizeTrackingLayout(t)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{sizeTrackingVolume(1, 1000, true)}, dn)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{sizeTrackingVolume(1, 2000, false)}, dn)
vl.accessLock.RLock()
st, tracked := vl.sizeTracking[needle.VolumeId(1)]
vl.accessLock.RUnlock()
if !tracked {
t.Fatal("a volume that became writable was not tracked again")
}
if st.reportedSize != 2000 {
t.Errorf("tracking resumed at size %d, want the reported 2000", st.reportedSize)
}
}
// A volume held out of the writable list for capacity is not read-only, and its
// entry is what enforces the recovery delay.
func TestSizeTrackingSurvivesAFullVolume(t *testing.T) {
topo, dn, vl := sizeTrackingLayout(t)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{sizeTrackingVolume(1, 1000, false)}, dn)
vl.RecordAssign(needle.VolumeId(1), int64(32*1024))
vl.accessLock.RLock()
st, tracked := vl.sizeTracking[needle.VolumeId(1)]
vl.accessLock.RUnlock()
if !tracked {
t.Fatal("a full volume lost its tracking, so nothing holds it out of the writable list")
}
if st.fullSince.IsZero() {
t.Error("a full volume did not record when it filled, so the recovery delay cannot apply")
}
}
// A volume is unwritable if any replica is read-only, so the answer must not
// depend on which replica's heartbeat arrives first.
func TestSizeTrackingIgnoresReplicaReportOrder(t *testing.T) {
for _, readOnlyFirst := range []bool{true, false} {
name := "WritableFirst"
if readOnlyFirst {
name = "ReadOnlyFirst"
}
t.Run(name, func(t *testing.T) {
topo := NewTopology("st", nil, 32*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
writableNode := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100})
readOnlyNode := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "b", map[string]uint32{"": 100})
report := func(dn *DataNode, readOnly bool) {
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
{Id: 1, Size: 1000, Collection: "c", Version: 3, ReplicaPlacement: 1, ReadOnly: readOnly},
}, dn)
}
if readOnlyFirst {
report(readOnlyNode, true)
report(writableNode, false)
} else {
report(writableNode, false)
report(readOnlyNode, true)
}
rp, _ := super_block.NewReplicaPlacementFromString("001")
vl := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)
vl.accessLock.RLock()
_, tracked := vl.sizeTracking[needle.VolumeId(1)]
_, crowded := vl.crowded[needle.VolumeId(1)]
vl.accessLock.RUnlock()
if tracked {
t.Error("a volume with a read-only replica is tracked for writes it cannot take")
}
if crowded {
t.Error("a volume with a read-only replica was left in the crowded set")
}
})
}
}
// The crowded entry has to go with the tracking, or the memory this releases
// is only moved.
func TestCrowdedEntryReleasedWithSizeTracking(t *testing.T) {
topo, dn, vl := sizeTrackingLayout(t)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{sizeTrackingVolume(1, 31000, false)}, dn)
vl.accessLock.RLock()
_, crowdedWhileWritable := vl.crowded[needle.VolumeId(1)]
vl.accessLock.RUnlock()
if !crowdedWhileWritable {
t.Fatal("expected a nearly full writable volume to be crowded")
}
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{sizeTrackingVolume(1, 31000, true)}, dn)
vl.accessLock.RLock()
_, stillCrowded := vl.crowded[needle.VolumeId(1)]
vl.accessLock.RUnlock()
if stillCrowded {
t.Error("a volume that became read-only kept its crowded entry")
}
}

View File

@@ -0,0 +1,120 @@
package topology
import (
"testing"
)
func flagNode(ip string) *DataNode {
dn := NewDataNode(ip)
dn.Ip, dn.Port = ip, 8080
return dn
}
func TestLocationFlagsFollowTheirNode(t *testing.T) {
a, b, c := flagNode("10.0.0.1"), flagNode("10.0.0.2"), flagNode("10.0.0.3")
list := NewVolumeLocationList()
for _, dn := range []*DataNode{a, b, c} {
list.Set(dn)
}
list.SetReadOnly(b, true)
list.SetOversized(c, true)
if !list.AnyReadOnly() || !list.AnyOversized() {
t.Fatal("flags did not register")
}
// Removing an earlier node shifts the rest down; the flags have to move with
// them or they end up describing the wrong server.
list.Remove(a)
list.SetReadOnly(b, false)
if list.AnyReadOnly() {
t.Error("clearing the flag on its node left it set, so it had shifted onto another")
}
if !list.AnyOversized() {
t.Error("removing an unrelated node dropped another node's flag")
}
list.SetOversized(c, false)
if list.AnyOversized() {
t.Error("clearing the flag on its node left it set")
}
}
func TestLocationFlagsClearedWithTheirNode(t *testing.T) {
a, b := flagNode("10.0.0.1"), flagNode("10.0.0.2")
list := NewVolumeLocationList()
list.Set(a)
list.Set(b)
list.SetReadOnly(a, true)
list.Remove(a)
if list.AnyReadOnly() {
t.Error("a departed node left its read-only marking behind")
}
}
// A node that shares an address replaces the entry, so it inherits the slot.
func TestLocationFlagsSurviveAReplacedNode(t *testing.T) {
a, replacement := flagNode("10.0.0.1"), flagNode("10.0.0.1")
list := NewVolumeLocationList()
list.Set(a)
list.SetReadOnly(a, true)
list.Set(replacement)
if !list.AnyReadOnly() {
t.Error("replacing the node at an address dropped what was known about the volume there")
}
list.SetReadOnly(replacement, false)
if list.AnyReadOnly() {
t.Error("the replacement could not clear the flag it inherited")
}
}
func TestLocationFlagsIgnoreUntrackableReplicaCounts(t *testing.T) {
list := NewVolumeLocationList()
nodes := make([]*DataNode, 0, maxTrackedLocations+2)
for i := 0; i < maxTrackedLocations+2; i++ {
dn := NewDataNode("n")
dn.Ip, dn.Port = "10.0.0.1", 9000+i
nodes = append(nodes, dn)
list.Set(dn)
}
// Past the tracked width the flag is not recorded rather than landing on
// some other node's slot.
list.SetReadOnly(nodes[maxTrackedLocations+1], true)
if list.AnyReadOnly() {
t.Error("a flag beyond the tracked width was recorded against another node")
}
list.SetReadOnly(nodes[0], true)
if !list.AnyReadOnly() {
t.Error("a flag within the tracked width was lost")
}
}
// Refresh drops stale locations, so it has to rebuild the flags with them.
func TestLocationFlagsRebuiltByRefresh(t *testing.T) {
stale, fresh, alsoFresh := flagNode("10.0.0.1"), flagNode("10.0.0.2"), flagNode("10.0.0.3")
stale.LastSeen, fresh.LastSeen, alsoFresh.LastSeen = 100, 500, 500
list := NewVolumeLocationList()
for _, dn := range []*DataNode{stale, fresh, alsoFresh} {
list.Set(dn)
}
list.SetReadOnly(alsoFresh, true)
list.Refresh(400)
if list.Length() != 2 {
t.Fatalf("expected the stale location to be dropped, got %d", list.Length())
}
// Both halves are needed: that the flag survived at all, and that it
// survived against the right location. Rebuilding the mask as empty would
// pass the second on its own.
if !list.AnyReadOnly() {
t.Fatal("the flag was dropped rather than carried across the rebuild")
}
list.SetReadOnly(alsoFresh, false)
if list.AnyReadOnly() {
t.Error("the flag did not move with its location, so it now describes another server")
}
}

View File

@@ -0,0 +1,194 @@
package topology
import (
"fmt"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
// maxTrackedLocations is how many replicas of one volume can carry per-node
// state. Beyond that the flags are simply not recorded, which loses a read-only
// or oversized marking rather than misreporting one.
const maxTrackedLocations = 32
type VolumeLocationList struct {
list []*DataNode
// readOnly and oversized mirror list by index, so the per-node state that
// used to need an index of its own rides along with the location it
// describes. Both are only ever asked whether any node reports them.
readOnly uint32
oversized uint32
}
func NewVolumeLocationList() *VolumeLocationList {
return &VolumeLocationList{}
}
func (dnll *VolumeLocationList) String() string {
return fmt.Sprintf("%v", dnll.list)
}
func (dnll *VolumeLocationList) Copy() *VolumeLocationList {
list := make([]*DataNode, len(dnll.list))
copy(list, dnll.list)
return &VolumeLocationList{
list: list,
readOnly: dnll.readOnly,
oversized: dnll.oversized,
}
}
func (dnll *VolumeLocationList) indexOf(loc *DataNode) int {
for i, dnl := range dnll.list {
if loc.Ip == dnl.Ip && loc.Port == dnl.Port {
return i
}
}
return -1
}
func setFlag(flags *uint32, index int, on bool) {
if index < 0 || index >= maxTrackedLocations {
return
}
if on {
*flags |= 1 << uint(index)
} else {
*flags &^= 1 << uint(index)
}
}
func hasFlag(flags uint32, index int) bool {
return index >= 0 && index < maxTrackedLocations && flags&(1<<uint(index)) != 0
}
// removeFlag drops the bit at index and closes the gap, keeping the flags
// aligned with the locations after one is removed.
func removeFlag(flags *uint32, index int) {
if index < 0 || index >= maxTrackedLocations {
return
}
low := *flags & (1<<uint(index) - 1)
high := *flags >> uint(index+1) << uint(index)
*flags = low | high
}
// SetReadOnly records whether loc reports the volume read-only.
func (dnll *VolumeLocationList) SetReadOnly(loc *DataNode, readOnly bool) {
setFlag(&dnll.readOnly, dnll.indexOf(loc), readOnly)
}
// AnyReadOnly reports whether any location has the volume read-only.
func (dnll *VolumeLocationList) AnyReadOnly() bool {
return dnll != nil && dnll.readOnly != 0
}
// SetOversized records whether loc reports the volume past the size limit.
func (dnll *VolumeLocationList) SetOversized(loc *DataNode, oversized bool) {
setFlag(&dnll.oversized, dnll.indexOf(loc), oversized)
}
// AnyOversized reports whether any location has the volume past the size limit.
func (dnll *VolumeLocationList) AnyOversized() bool {
return dnll != nil && dnll.oversized != 0
}
func (dnll *VolumeLocationList) Head() *DataNode {
//mark first node as master volume
if dnll.Length() == 0 {
return nil
}
return dnll.list[0]
}
func (dnll *VolumeLocationList) Rest() []*DataNode {
//mark first node as master volume
return dnll.list[1:]
}
func (dnll *VolumeLocationList) Length() int {
if dnll == nil {
return 0
}
return len(dnll.list)
}
// Set adds loc, or replaces the entry at the same address, returning the node it
// displaced. Two volume servers can share an address -- GetOrCreateDataNode keys
// on the reported id and refuses to merge a new id onto an address an older node
// still claims -- so the displaced node is not necessarily loc, and callers
// tracking which node a volume is reachable through must move their bookkeeping
// off it rather than assume loc already owned the entry.
func (dnll *VolumeLocationList) Set(loc *DataNode) (displaced *DataNode) {
for i := 0; i < len(dnll.list); i++ {
if loc.Ip == dnll.list[i].Ip && loc.Port == dnll.list[i].Port {
displaced = dnll.list[i]
dnll.list[i] = loc
return displaced
}
}
dnll.list = append(dnll.list, loc)
return nil
}
// Remove drops the entry at loc's address and returns the node removed, or nil
// if the volume was not reachable there. As with Set, the removed node is
// matched by address and need not be loc.
func (dnll *VolumeLocationList) Remove(loc *DataNode) (removed *DataNode) {
for i, dnl := range dnll.list {
if loc.Ip == dnl.Ip && loc.Port == dnl.Port {
dnll.list = append(dnll.list[:i], dnll.list[i+1:]...)
removeFlag(&dnll.readOnly, i)
removeFlag(&dnll.oversized, i)
return dnl
}
}
return nil
}
func (dnll *VolumeLocationList) Refresh(freshThreshHold int64) {
var changed bool
for _, dnl := range dnll.list {
if dnl.LastSeen < freshThreshHold {
changed = true
break
}
}
if !changed {
return
}
// The flags index the locations, so dropping some means rebuilding both
// rather than leaving bits describing whoever moved into their place.
var l []*DataNode
var readOnly, oversized uint32
for i, dnl := range dnll.list {
if dnl.LastSeen < freshThreshHold {
continue
}
setFlag(&readOnly, len(l), hasFlag(dnll.readOnly, i))
setFlag(&oversized, len(l), hasFlag(dnll.oversized, i))
l = append(l, dnl)
}
dnll.list, dnll.readOnly, dnll.oversized = l, readOnly, oversized
}
// Stats returns logic size and count. Both subtractions are clamped: the
// deleted counters are maintained apart from the totals they come off and can
// transiently exceed them.
func (dnll *VolumeLocationList) Stats(vid needle.VolumeId, freshThreshHold int64) (size uint64, fileCount int) {
for _, dnl := range dnll.list {
if dnl.LastSeen < freshThreshHold {
vinfo, err := dnl.GetVolumesById(vid)
if err == nil {
if vinfo.Size > vinfo.DeletedByteCount {
size = vinfo.Size - vinfo.DeletedByteCount
}
if vinfo.FileCount > vinfo.DeleteCount {
fileCount = int(vinfo.FileCount - vinfo.DeleteCount)
}
return size, fileCount
}
}
}
return 0, 0
}

View File

@@ -0,0 +1,137 @@
package topology
import (
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
// StreamVolumes hands every volume the filter selects to send, in batches, so
// that neither end holds the whole cluster to move it. Batches are built under
// their disk's lock and handed over outside it, so a slow reader stalls the
// stream rather than the topology.
//
// Only the disks `listed` names are streamed. That listing went out first, and
// the topology is walked again here, so without it a disk registering in
// between would have its volumes sent to a client with nowhere to put them.
// Bounding the walk by what was already announced makes the two agree by
// construction: a disk arriving mid-listing is in neither, and is reported by
// the next one.
//
// The batches still do not share one instant. Neither did a single listing,
// which takes each disk's lock in turn, so a volume that moves while either is
// running can be seen twice or not at all.
func (t *Topology) StreamVolumes(listed *master_pb.TopologyInfo, filter VolumeFilter, batchSize int, send func(*master_pb.VolumeListStreamResponse) error) error {
if batchSize <= 0 {
batchSize = defaultVolumeStreamBatch
}
announced := announcedDisks(listed)
for _, dcNode := range t.Children() {
dc := dcNode.(*DataCenter)
for _, rackNode := range dc.Children() {
rack := rackNode.(*Rack)
for _, dnNode := range rack.Children() {
dn := dnNode.(*DataNode)
for _, diskNode := range dn.Children() {
disk := diskNode.(*Disk)
if !announced[[4]string{string(dc.Id()), string(rack.Id()), string(dn.Id()), string(disk.Id())}] {
continue
}
batch := func() *master_pb.VolumeListStreamResponse {
return &master_pb.VolumeListStreamResponse{
DataCenter: string(dc.Id()),
Rack: string(rack.Id()),
DataNode: string(dn.Id()),
DiskType: string(disk.Id()),
}
}
if err := disk.streamVolumes(filter, batchSize, batch, send); err != nil {
return err
}
if err := disk.streamEcShards(filter, batchSize, batch, send); err != nil {
return err
}
}
}
}
}
return nil
}
const defaultVolumeStreamBatch = 10000
// announcedDisks names the disks a listing carried.
func announcedDisks(listed *master_pb.TopologyInfo) map[[4]string]bool {
announced := make(map[[4]string]bool)
if listed == nil {
return announced
}
for _, dc := range listed.DataCenterInfos {
for _, rack := range dc.RackInfos {
for _, node := range rack.DataNodeInfos {
for diskType := range node.DiskInfos {
announced[[4]string{dc.Id, rack.Id, node.Id, diskType}] = true
}
}
}
}
return announced
}
// streamVolumes sends this disk's volumes a batch at a time. The ids are taken
// in one pass and the messages built in later ones, so the lock is held for a
// batch rather than for the disk, and only 4 bytes per volume are carried
// between passes. A volume that leaves in between is simply not sent.
func (d *Disk) streamVolumes(filter VolumeFilter, batchSize int, newBatch func() *master_pb.VolumeListStreamResponse, send func(*master_pb.VolumeListStreamResponse) error) error {
d.RLock()
ids := make([]needle.VolumeId, 0, len(d.volumes))
for id := range d.volumes {
ids = append(ids, id)
}
d.RUnlock()
for start := 0; start < len(ids); start += batchSize {
end := min(start+batchSize, len(ids))
batch := newBatch()
d.RLock()
for _, id := range ids[start:end] {
v, found := d.volumes[id]
if !found || !filter.matches(v) {
continue
}
batch.VolumeInfos = append(batch.VolumeInfos, v.ToVolumeInformationMessage())
}
d.RUnlock()
if len(batch.VolumeInfos) == 0 {
continue
}
if err := send(batch); err != nil {
return err
}
}
return nil
}
func (d *Disk) streamEcShards(filter VolumeFilter, batchSize int, newBatch func() *master_pb.VolumeListStreamResponse, send func(*master_pb.VolumeListStreamResponse) error) error {
// GetEcShards already copies under the lock, and a cluster holds far fewer
// ec shards than volumes, so these only need cutting into batches.
shards := d.GetEcShards()
for start := 0; start < len(shards); start += batchSize {
end := min(start+batchSize, len(shards))
batch := newBatch()
for _, ecv := range shards[start:end] {
if !filter.matches(ecv) {
continue
}
batch.EcShardInfos = append(batch.EcShardInfos, ecv.ToVolumeEcShardInformationMessage())
}
if len(batch.EcShardInfos) == 0 {
continue
}
if err := send(batch); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,45 @@
package topology
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
)
// A disk that registers after the listing went out has nowhere to go in it, so
// its volumes must not be streamed into a client that cannot place them. It is
// reported by the next listing instead.
func TestStreamVolumesStaysInsideTheAnnouncedTopology(t *testing.T) {
topo := NewTopology("bounds", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
known := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
{Id: 1, Collection: "c", Size: 100, Version: 3},
{Id: 2, Collection: "c", Size: 200, Version: 3},
}, known)
listed := topo.ToTopologyInfo(NoVolumes())
// The heartbeat that lands between the listing and the walk below.
late := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
{Id: 3, Collection: "c", Size: 300, Version: 3},
}, late)
var streamed []uint32
err := topo.StreamVolumes(listed, VolumeFilter{}, 10, func(b *master_pb.VolumeListStreamResponse) error {
if b.DataNode == string(late.Id()) {
t.Errorf("streamed a batch for %s, which the listing never named", b.DataNode)
}
for _, v := range b.VolumeInfos {
streamed = append(streamed, v.Id)
}
return nil
})
if err != nil {
t.Fatal(err)
}
if !equalIds(streamed, []uint32{1, 2}) {
t.Errorf("streamed %v, want the two volumes the listing named", streamed)
}
}

View File

@@ -0,0 +1,134 @@
package topology
import (
"fmt"
"sort"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
)
func streamTestTopology(t *testing.T) *Topology {
t.Helper()
topo := NewTopology("stream", nil, 32*1024*1024*1024, 5, false)
for dc := 1; dc <= 2; dc++ {
rack := topo.GetOrCreateDataCenter(fmt.Sprintf("dc%d", dc)).GetOrCreateRack("rack1")
for n := 1; n <= 2; n++ {
dn := rack.GetOrCreateDataNode(fmt.Sprintf("10.0.%d.%d", dc, n), 8080, 18080, "", "",
map[string]uint32{"": 10000})
var volumes []*master_pb.VolumeInformationMessage
for i := 0; i < 250; i++ {
volumes = append(volumes, &master_pb.VolumeInformationMessage{
Id: uint32(dc*10000 + n*1000 + i), Size: uint64(i) * 100,
Collection: fmt.Sprintf("c%d", i%3), Version: 3, DiskId: uint32(i % 2),
})
}
topo.SyncDataNodeRegistration(volumes, dn)
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: uint32(dc*100 + n), Collection: "c1", EcIndexBits: 0x3fff, DiskId: 1},
}, dn)
}
}
return topo
}
// summarise reduces a listing to what a caller reads off it, so a streamed one
// can be held against an unstreamed one without depending on ordering.
func summarise(info *master_pb.TopologyInfo) []string {
var lines []string
for _, dc := range info.DataCenterInfos {
for _, rack := range dc.RackInfos {
for _, node := range rack.DataNodeInfos {
for diskType, disk := range node.DiskInfos {
where := fmt.Sprintf("%s/%s/%s/%s", dc.Id, rack.Id, node.Id, diskType)
lines = append(lines,
fmt.Sprintf("%s disk id=%d max=%d count=%d", where, disk.DiskId, disk.MaxVolumeCount, disk.VolumeCount))
for _, v := range disk.VolumeInfos {
lines = append(lines, fmt.Sprintf("%s vol %d size=%d collection=%s disk=%d",
where, v.Id, v.Size, v.Collection, v.DiskId))
}
for _, ec := range disk.EcShardInfos {
lines = append(lines, fmt.Sprintf("%s ec %d collection=%s bits=%d",
where, ec.Id, ec.Collection, ec.EcIndexBits))
}
}
}
}
}
sort.Strings(lines)
return lines
}
// Every caller that moved onto the stream rebuilds the listing from a topology
// sent without volumes plus the batches that follow. That has to come out the
// same as the listing they used to be sent whole.
func TestStreamedListingRebuildsToTheSameThing(t *testing.T) {
topo := streamTestTopology(t)
for _, batchSize := range []int{1, 7, 250, 100000} {
t.Run(fmt.Sprintf("batch=%d", batchSize), func(t *testing.T) {
rebuilt := topo.ToTopologyInfo(NoVolumes())
byPlace := map[[4]string]*master_pb.DiskInfo{}
for _, dc := range rebuilt.DataCenterInfos {
for _, rack := range dc.RackInfos {
for _, node := range rack.DataNodeInfos {
for diskType, disk := range node.DiskInfos {
byPlace[[4]string{dc.Id, rack.Id, node.Id, diskType}] = disk
if len(disk.VolumeInfos) != 0 || len(disk.EcShardInfos) != 0 {
t.Fatalf("the header listed volumes on %s", node.Id)
}
}
}
}
}
err := topo.StreamVolumes(rebuilt, VolumeFilter{}, batchSize, func(b *master_pb.VolumeListStreamResponse) error {
if batchSize < 100000 && len(b.VolumeInfos) > batchSize {
t.Errorf("batch carried %d volumes, more than the %d asked for", len(b.VolumeInfos), batchSize)
}
disk := byPlace[[4]string{b.DataCenter, b.Rack, b.DataNode, b.DiskType}]
if disk == nil {
t.Fatalf("batch named a disk the header did not: %s/%s", b.DataNode, b.DiskType)
}
disk.VolumeInfos = append(disk.VolumeInfos, b.VolumeInfos...)
disk.EcShardInfos = append(disk.EcShardInfos, b.EcShardInfos...)
return nil
})
if err != nil {
t.Fatal(err)
}
want := summarise(topo.ToTopologyInfo(VolumeFilter{}))
got := summarise(rebuilt)
if len(got) != len(want) {
t.Fatalf("rebuilt %d lines, want %d", len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("line %d:\n got %q\nwant %q", i, got[i], want[i])
}
}
})
}
}
// A filter must select the same volumes streamed as it does whole.
func TestStreamedListingHonoursTheFilter(t *testing.T) {
topo := streamTestTopology(t)
name := "c1"
var streamed []uint32
err := topo.StreamVolumes(topo.ToTopologyInfo(NoVolumes()), VolumeFilter{Collection: &name}, 16, func(b *master_pb.VolumeListStreamResponse) error {
for _, v := range b.VolumeInfos {
streamed = append(streamed, v.Id)
}
return nil
})
if err != nil {
t.Fatal(err)
}
whole, _ := listed(topo.ToTopologyInfo(VolumeFilter{Collection: &name}))
if !equalIds(streamed, whole) {
t.Errorf("streamed %d volumes, whole listing had %d", len(streamed), len(whole))
}
}