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
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:
19
weed/admin/topology/active_topology.go
Normal file
19
weed/admin/topology/active_topology.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package topology
|
||||
|
||||
// NewActiveTopology creates a new ActiveTopology instance
|
||||
func NewActiveTopology(recentTaskWindowSeconds int) *ActiveTopology {
|
||||
if recentTaskWindowSeconds <= 0 {
|
||||
recentTaskWindowSeconds = 10 // Default 10 seconds
|
||||
}
|
||||
|
||||
return &ActiveTopology{
|
||||
nodes: make(map[string]*activeNode),
|
||||
disks: make(map[string]*activeDisk),
|
||||
volumeIndex: make(map[uint32][]string),
|
||||
ecShardIndex: make(map[uint32][]string),
|
||||
pendingTasks: make(map[string]*taskState),
|
||||
assignedTasks: make(map[string]*taskState),
|
||||
recentTasks: make(map[string]*taskState),
|
||||
recentTaskWindowSeconds: recentTaskWindowSeconds,
|
||||
}
|
||||
}
|
||||
755
weed/admin/topology/active_topology_test.go
Normal file
755
weed/admin/topology/active_topology_test.go
Normal file
@@ -0,0 +1,755 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Helper function to find a disk by ID for testing - reduces code duplication
|
||||
func findDiskByID(disks []*DiskInfo, diskID uint32) *DiskInfo {
|
||||
for _, disk := range disks {
|
||||
if disk.DiskID == diskID {
|
||||
return disk
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestActiveTopologyBasicOperations tests basic topology management
|
||||
func TestActiveTopologyBasicOperations(t *testing.T) {
|
||||
topology := NewActiveTopology(10)
|
||||
assert.NotNil(t, topology)
|
||||
assert.Equal(t, 10, topology.recentTaskWindowSeconds)
|
||||
|
||||
// Test empty topology
|
||||
assert.Equal(t, 0, len(topology.nodes))
|
||||
assert.Equal(t, 0, len(topology.disks))
|
||||
assert.Equal(t, 0, len(topology.pendingTasks))
|
||||
}
|
||||
|
||||
// TestActiveTopologyUpdate tests topology updates from master
|
||||
func TestActiveTopologyUpdate(t *testing.T) {
|
||||
topology := NewActiveTopology(10)
|
||||
|
||||
// Create sample topology info
|
||||
topologyInfo := createSampleTopology()
|
||||
|
||||
err := topology.UpdateTopology(topologyInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify topology structure
|
||||
assert.Equal(t, 2, len(topology.nodes)) // 2 nodes
|
||||
assert.Equal(t, 4, len(topology.disks)) // 4 disks total (2 per node)
|
||||
|
||||
// Verify node structure
|
||||
node1, exists := topology.nodes["10.0.0.1:8080"]
|
||||
require.True(t, exists)
|
||||
assert.Equal(t, "dc1", node1.dataCenter)
|
||||
assert.Equal(t, "rack1", node1.rack)
|
||||
assert.Equal(t, 2, len(node1.disks))
|
||||
|
||||
// Verify disk structure
|
||||
disk1, exists := topology.disks["10.0.0.1:8080:0"]
|
||||
require.True(t, exists)
|
||||
assert.Equal(t, uint32(0), disk1.DiskID)
|
||||
assert.Equal(t, "hdd", disk1.DiskType)
|
||||
assert.Equal(t, "dc1", disk1.DataCenter)
|
||||
}
|
||||
|
||||
// TestTaskLifecycle tests the complete task lifecycle
|
||||
func TestTaskLifecycle(t *testing.T) {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createSampleTopology())
|
||||
|
||||
taskID := "balance-001"
|
||||
|
||||
// 1. Add pending task
|
||||
err := topology.AddPendingTask(TaskSpec{
|
||||
TaskID: taskID,
|
||||
TaskType: TaskTypeBalance,
|
||||
VolumeID: 1001,
|
||||
VolumeSize: 1024 * 1024 * 1024,
|
||||
Sources: []TaskSourceSpec{
|
||||
{ServerID: "10.0.0.1:8080", DiskID: 0},
|
||||
},
|
||||
Destinations: []TaskDestinationSpec{
|
||||
{ServerID: "10.0.0.2:8080", DiskID: 1},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err, "Should add pending task successfully")
|
||||
|
||||
// Verify pending state
|
||||
assert.Equal(t, 1, len(topology.pendingTasks))
|
||||
assert.Equal(t, 0, len(topology.assignedTasks))
|
||||
assert.Equal(t, 0, len(topology.recentTasks))
|
||||
|
||||
task := topology.pendingTasks[taskID]
|
||||
assert.Equal(t, TaskStatusPending, task.Status)
|
||||
assert.Equal(t, uint32(1001), task.VolumeID)
|
||||
|
||||
// Verify task assigned to disks
|
||||
sourceDisk := topology.disks["10.0.0.1:8080:0"]
|
||||
targetDisk := topology.disks["10.0.0.2:8080:1"]
|
||||
assert.Equal(t, 1, len(sourceDisk.pendingTasks))
|
||||
assert.Equal(t, 1, len(targetDisk.pendingTasks))
|
||||
|
||||
// 2. Assign task
|
||||
err = topology.AssignTask(taskID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify assigned state
|
||||
assert.Equal(t, 0, len(topology.pendingTasks))
|
||||
assert.Equal(t, 1, len(topology.assignedTasks))
|
||||
assert.Equal(t, 0, len(topology.recentTasks))
|
||||
|
||||
task = topology.assignedTasks[taskID]
|
||||
assert.Equal(t, TaskStatusInProgress, task.Status)
|
||||
|
||||
// Verify task moved to assigned on disks
|
||||
assert.Equal(t, 0, len(sourceDisk.pendingTasks))
|
||||
assert.Equal(t, 1, len(sourceDisk.assignedTasks))
|
||||
assert.Equal(t, 0, len(targetDisk.pendingTasks))
|
||||
assert.Equal(t, 1, len(targetDisk.assignedTasks))
|
||||
|
||||
// 3. Complete task
|
||||
err = topology.CompleteTask(taskID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify completed state
|
||||
assert.Equal(t, 0, len(topology.pendingTasks))
|
||||
assert.Equal(t, 0, len(topology.assignedTasks))
|
||||
assert.Equal(t, 1, len(topology.recentTasks))
|
||||
|
||||
task = topology.recentTasks[taskID]
|
||||
assert.Equal(t, TaskStatusCompleted, task.Status)
|
||||
assert.False(t, task.CompletedAt.IsZero())
|
||||
}
|
||||
|
||||
// TestTaskDetectionScenarios tests various task detection scenarios
|
||||
func TestTaskDetectionScenarios(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scenario func() *ActiveTopology
|
||||
expectedTasks map[string]bool // taskType -> shouldDetect
|
||||
}{
|
||||
{
|
||||
name: "Empty cluster - no tasks needed",
|
||||
scenario: func() *ActiveTopology {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createEmptyTopology())
|
||||
return topology
|
||||
},
|
||||
expectedTasks: map[string]bool{
|
||||
"balance": false,
|
||||
"vacuum": false,
|
||||
"ec": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unbalanced cluster - balance task needed",
|
||||
scenario: func() *ActiveTopology {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createUnbalancedTopology())
|
||||
return topology
|
||||
},
|
||||
expectedTasks: map[string]bool{
|
||||
"balance": true,
|
||||
"vacuum": false,
|
||||
"ec": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "High garbage ratio - vacuum task needed",
|
||||
scenario: func() *ActiveTopology {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createHighGarbageTopology())
|
||||
return topology
|
||||
},
|
||||
expectedTasks: map[string]bool{
|
||||
"balance": false,
|
||||
"vacuum": true,
|
||||
"ec": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Large volumes - EC task needed",
|
||||
scenario: func() *ActiveTopology {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createLargeVolumeTopology())
|
||||
return topology
|
||||
},
|
||||
expectedTasks: map[string]bool{
|
||||
"balance": false,
|
||||
"vacuum": false,
|
||||
"ec": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Recent tasks - no immediate re-detection",
|
||||
scenario: func() *ActiveTopology {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createUnbalancedTopology())
|
||||
// Add recent balance task
|
||||
topology.recentTasks["recent-balance"] = &taskState{
|
||||
VolumeID: 1001,
|
||||
TaskType: TaskTypeBalance,
|
||||
Status: TaskStatusCompleted,
|
||||
CompletedAt: time.Now().Add(-5 * time.Second), // 5 seconds ago
|
||||
}
|
||||
return topology
|
||||
},
|
||||
expectedTasks: map[string]bool{
|
||||
"balance": false, // Should not detect due to recent task
|
||||
"vacuum": false,
|
||||
"ec": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
topology := tt.scenario()
|
||||
|
||||
// Test balance task detection
|
||||
shouldDetectBalance := tt.expectedTasks["balance"]
|
||||
actualDetectBalance := !topology.HasRecentTaskForVolume(1001, TaskTypeBalance)
|
||||
if shouldDetectBalance {
|
||||
assert.True(t, actualDetectBalance, "Should detect balance task")
|
||||
} else {
|
||||
// Note: In real implementation, task detection would be more sophisticated
|
||||
// This is a simplified test of the recent task prevention mechanism
|
||||
}
|
||||
|
||||
// Test that recent tasks prevent re-detection
|
||||
if len(topology.recentTasks) > 0 {
|
||||
for _, task := range topology.recentTasks {
|
||||
hasRecent := topology.HasRecentTaskForVolume(task.VolumeID, task.TaskType)
|
||||
assert.True(t, hasRecent, "Should find recent task for volume %d", task.VolumeID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetSelectionScenarios tests target selection for different task types
|
||||
func TestTargetSelectionScenarios(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
topology *ActiveTopology
|
||||
taskType TaskType
|
||||
excludeNode string
|
||||
expectedTargets int
|
||||
expectedBestTarget string
|
||||
}{
|
||||
{
|
||||
name: "Balance task - find least loaded disk",
|
||||
topology: createTopologyWithLoad(),
|
||||
taskType: TaskTypeBalance,
|
||||
excludeNode: "10.0.0.1:8080", // Exclude source node
|
||||
expectedTargets: 2, // 2 disks on other node
|
||||
},
|
||||
{
|
||||
name: "EC task - find multiple available disks",
|
||||
topology: createTopologyForEC(),
|
||||
taskType: TaskTypeErasureCoding,
|
||||
excludeNode: "", // Don't exclude any nodes
|
||||
expectedTargets: 4, // All 4 disks available
|
||||
},
|
||||
{
|
||||
name: "Vacuum task - cross-type tasks do not block per disk",
|
||||
topology: createTopologyWithConflicts(),
|
||||
taskType: TaskTypeVacuum,
|
||||
excludeNode: "",
|
||||
expectedTargets: 4, // All 4 disks available; per-volume safety is enforced by HasAnyTask
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
availableDisks := tt.topology.GetAvailableDisks(tt.taskType, tt.excludeNode)
|
||||
assert.Equal(t, tt.expectedTargets, len(availableDisks),
|
||||
"Expected %d available disks, got %d", tt.expectedTargets, len(availableDisks))
|
||||
|
||||
// Verify disks are actually available
|
||||
for _, disk := range availableDisks {
|
||||
assert.NotEqual(t, tt.excludeNode, disk.NodeID,
|
||||
"Available disk should not be on excluded node")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDiskLoadCalculation tests disk load calculation
|
||||
func TestDiskLoadCalculation(t *testing.T) {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createSampleTopology())
|
||||
|
||||
// Initially no load
|
||||
disks := topology.GetNodeDisks("10.0.0.1:8080")
|
||||
targetDisk := findDiskByID(disks, 0)
|
||||
require.NotNil(t, targetDisk, "Should find disk with ID 0")
|
||||
assert.Equal(t, 0, targetDisk.LoadCount)
|
||||
|
||||
// Add pending task
|
||||
err := topology.AddPendingTask(TaskSpec{
|
||||
TaskID: "task1",
|
||||
TaskType: TaskTypeBalance,
|
||||
VolumeID: 1001,
|
||||
VolumeSize: 1024 * 1024 * 1024,
|
||||
Sources: []TaskSourceSpec{
|
||||
{ServerID: "10.0.0.1:8080", DiskID: 0},
|
||||
},
|
||||
Destinations: []TaskDestinationSpec{
|
||||
{ServerID: "10.0.0.2:8080", DiskID: 1},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err, "Should add pending task successfully")
|
||||
|
||||
// Check load increased
|
||||
disks = topology.GetNodeDisks("10.0.0.1:8080")
|
||||
targetDisk = findDiskByID(disks, 0)
|
||||
assert.Equal(t, 1, targetDisk.LoadCount)
|
||||
|
||||
// Add another task to same disk
|
||||
err = topology.AddPendingTask(TaskSpec{
|
||||
TaskID: "task2",
|
||||
TaskType: TaskTypeVacuum,
|
||||
VolumeID: 1002,
|
||||
VolumeSize: 0,
|
||||
Sources: []TaskSourceSpec{
|
||||
{ServerID: "10.0.0.1:8080", DiskID: 0},
|
||||
},
|
||||
Destinations: []TaskDestinationSpec{
|
||||
{ServerID: "", DiskID: 0}, // Vacuum doesn't have a destination
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err, "Should add vacuum task successfully")
|
||||
|
||||
disks = topology.GetNodeDisks("10.0.0.1:8080")
|
||||
targetDisk = findDiskByID(disks, 0)
|
||||
assert.Equal(t, 2, targetDisk.LoadCount)
|
||||
|
||||
// Move one task to assigned
|
||||
topology.AssignTask("task1")
|
||||
|
||||
// Load should still be 2 (1 pending + 1 assigned)
|
||||
disks = topology.GetNodeDisks("10.0.0.1:8080")
|
||||
targetDisk = findDiskByID(disks, 0)
|
||||
assert.Equal(t, 2, targetDisk.LoadCount)
|
||||
|
||||
// Complete one task
|
||||
topology.CompleteTask("task1")
|
||||
|
||||
// Load should decrease to 1
|
||||
disks = topology.GetNodeDisks("10.0.0.1:8080")
|
||||
targetDisk = findDiskByID(disks, 0)
|
||||
assert.Equal(t, 1, targetDisk.LoadCount)
|
||||
}
|
||||
|
||||
func TestCrossTypeTasksDoNotBlockPerDisk(t *testing.T) {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createSampleTopology())
|
||||
|
||||
err := topology.AddPendingTask(TaskSpec{
|
||||
TaskID: "balance1",
|
||||
TaskType: TaskTypeBalance,
|
||||
VolumeID: 1001,
|
||||
VolumeSize: 1024 * 1024 * 1024,
|
||||
Sources: []TaskSourceSpec{
|
||||
{ServerID: "10.0.0.1:8080", DiskID: 0},
|
||||
},
|
||||
Destinations: []TaskDestinationSpec{
|
||||
{ServerID: "10.0.0.2:8080", DiskID: 1},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, topology.AssignTask("balance1"))
|
||||
|
||||
availableDisks := topology.GetAvailableDisks(TaskTypeVacuum, "")
|
||||
sourceDiskAvailable := false
|
||||
for _, disk := range availableDisks {
|
||||
if disk.NodeID == "10.0.0.1:8080" && disk.DiskID == 0 {
|
||||
sourceDiskAvailable = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, sourceDiskAvailable,
|
||||
"Source disk should remain available for an unrelated task type")
|
||||
}
|
||||
|
||||
// Regression for #9147: a 4-disk cluster with one in-flight balance task must
|
||||
// still expose all 4 disks to EC placement so MinTotalDisks can be satisfied.
|
||||
func TestECPlanningNotBlockedByUnrelatedBalance(t *testing.T) {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createSampleTopology()) // 2 nodes x 2 disks
|
||||
|
||||
err := topology.AddPendingTask(TaskSpec{
|
||||
TaskID: "balance1",
|
||||
TaskType: TaskTypeBalance,
|
||||
VolumeID: 42,
|
||||
VolumeSize: 1024 * 1024 * 1024,
|
||||
Sources: []TaskSourceSpec{
|
||||
{ServerID: "10.0.0.1:8080", DiskID: 0},
|
||||
},
|
||||
Destinations: []TaskDestinationSpec{
|
||||
{ServerID: "10.0.0.2:8080", DiskID: 0},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, topology.AssignTask("balance1"))
|
||||
|
||||
ecCandidates := topology.GetDisksWithEffectiveCapacity(TaskTypeErasureCoding, "", 0)
|
||||
assert.Equal(t, 4, len(ecCandidates),
|
||||
"EC must still see all 4 disks even with an unrelated in-flight balance")
|
||||
}
|
||||
|
||||
// #9369: same-type physical disks collapse into one DiskInfo at the master;
|
||||
// the active topology must still expose one entry per physical disk_id.
|
||||
func TestECPlannerSeesEachPhysicalDisk(t *testing.T) {
|
||||
topology := NewActiveTopology(10)
|
||||
|
||||
const numServers = 7
|
||||
const disksPerServer = 2
|
||||
nodes := make([]*master_pb.DataNodeInfo, 0, numServers)
|
||||
for i := 1; i <= numServers; i++ {
|
||||
volumeInfos := make([]*master_pb.VolumeInformationMessage, 0, disksPerServer)
|
||||
for d := uint32(0); d < disksPerServer; d++ {
|
||||
volumeInfos = append(volumeInfos, &master_pb.VolumeInformationMessage{
|
||||
Id: uint32(i*10 + int(d)),
|
||||
DiskId: d,
|
||||
DiskType: "hdd",
|
||||
})
|
||||
}
|
||||
nodes = append(nodes, &master_pb.DataNodeInfo{
|
||||
Id: fmt.Sprintf("127.0.0.1:%d", 8080+i),
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"hdd": {
|
||||
DiskId: 0,
|
||||
VolumeCount: int64(disksPerServer),
|
||||
MaxVolumeCount: 200,
|
||||
VolumeInfos: volumeInfos,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
require.NoError(t, topology.UpdateTopology(&master_pb.TopologyInfo{
|
||||
DataCenterInfos: []*master_pb.DataCenterInfo{{
|
||||
Id: "dc1",
|
||||
RackInfos: []*master_pb.RackInfo{{
|
||||
Id: "rack1",
|
||||
DataNodeInfos: nodes,
|
||||
}},
|
||||
}},
|
||||
}))
|
||||
|
||||
candidates := topology.GetDisksWithEffectiveCapacity(TaskTypeErasureCoding, "", 0)
|
||||
assert.Equal(t, numServers*disksPerServer, len(candidates))
|
||||
|
||||
seen := make(map[string]bool, len(candidates))
|
||||
for _, c := range candidates {
|
||||
key := fmt.Sprintf("%s:%d", c.NodeID, c.DiskID)
|
||||
assert.False(t, seen[key], "duplicate placement target %s", key)
|
||||
seen[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestVolumeIndexResolvesPhysicalDiskZero pins that a volume genuinely on
|
||||
// physical disk 0 resolves to its own disk via GetVolumeLocations even when the
|
||||
// aggregate DiskInfo.DiskId is seeded from a non-zero sibling (as ToDiskInfo
|
||||
// does from volumes[0]). Re-deriving the per-record disk id would fold disk 0
|
||||
// onto the sibling, then the lookup would find no matching volume and drop it.
|
||||
func TestVolumeIndexResolvesPhysicalDiskZero(t *testing.T) {
|
||||
topology := NewActiveTopology(10)
|
||||
|
||||
require.NoError(t, topology.UpdateTopology(&master_pb.TopologyInfo{
|
||||
DataCenterInfos: []*master_pb.DataCenterInfo{{
|
||||
Id: "dc1",
|
||||
RackInfos: []*master_pb.RackInfo{{
|
||||
Id: "rack1",
|
||||
DataNodeInfos: []*master_pb.DataNodeInfo{{
|
||||
Id: "127.0.0.1:8080",
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"hdd": {
|
||||
DiskId: 6, // seeded from volumes[0].DiskId
|
||||
MaxVolumeCount: 200,
|
||||
VolumeInfos: []*master_pb.VolumeInformationMessage{
|
||||
{Id: 11, DiskId: 6, DiskType: "hdd"},
|
||||
{Id: 10, DiskId: 0, DiskType: "hdd"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}))
|
||||
|
||||
loc10 := topology.GetVolumeLocations(10, "")
|
||||
require.Len(t, loc10, 1, "volume on physical disk 0 must resolve to one disk")
|
||||
assert.Equal(t, uint32(0), loc10[0].DiskID, "volume 10 lives on physical disk 0")
|
||||
|
||||
loc11 := topology.GetVolumeLocations(11, "")
|
||||
require.Len(t, loc11, 1)
|
||||
assert.Equal(t, uint32(6), loc11[0].DiskID, "volume 11 lives on physical disk 6")
|
||||
}
|
||||
|
||||
// TestECShardIndexResolvesPhysicalDiskZero is the EC-shard twin of
|
||||
// TestVolumeIndexResolvesPhysicalDiskZero: rebuildIndexes builds ecShardIndex
|
||||
// the same way, so an EC shard on physical disk 0 must resolve via
|
||||
// GetECShardLocations rather than being folded onto a non-zero sibling.
|
||||
func TestECShardIndexResolvesPhysicalDiskZero(t *testing.T) {
|
||||
topology := NewActiveTopology(10)
|
||||
|
||||
require.NoError(t, topology.UpdateTopology(&master_pb.TopologyInfo{
|
||||
DataCenterInfos: []*master_pb.DataCenterInfo{{
|
||||
Id: "dc1",
|
||||
RackInfos: []*master_pb.RackInfo{{
|
||||
Id: "rack1",
|
||||
DataNodeInfos: []*master_pb.DataNodeInfo{{
|
||||
Id: "127.0.0.1:8080",
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"hdd": {
|
||||
DiskId: 6, // seeded from a non-zero sibling
|
||||
MaxVolumeCount: 200,
|
||||
EcShardInfos: []*master_pb.VolumeEcShardInformationMessage{
|
||||
{Id: 21, DiskId: 6, Collection: "c1", EcIndexBits: 0b1},
|
||||
{Id: 20, DiskId: 0, Collection: "c1", EcIndexBits: 0b1},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}))
|
||||
|
||||
loc20 := topology.GetECShardLocations(20, "c1")
|
||||
require.Len(t, loc20, 1, "EC shard on physical disk 0 must resolve to one disk")
|
||||
assert.Equal(t, uint32(0), loc20[0].DiskID, "shard 20 lives on physical disk 0")
|
||||
|
||||
loc21 := topology.GetECShardLocations(21, "c1")
|
||||
require.Len(t, loc21, 1)
|
||||
assert.Equal(t, uint32(6), loc21[0].DiskID, "shard 21 lives on physical disk 6")
|
||||
}
|
||||
|
||||
// TestPublicInterfaces tests the public interface methods
|
||||
func TestPublicInterfaces(t *testing.T) {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createSampleTopology())
|
||||
|
||||
// Test GetAllNodes
|
||||
nodes := topology.GetAllNodes()
|
||||
assert.Equal(t, 2, len(nodes))
|
||||
assert.Contains(t, nodes, "10.0.0.1:8080")
|
||||
assert.Contains(t, nodes, "10.0.0.2:8080")
|
||||
|
||||
// Test GetNodeDisks
|
||||
disks := topology.GetNodeDisks("10.0.0.1:8080")
|
||||
assert.Equal(t, 2, len(disks))
|
||||
|
||||
// Test with non-existent node
|
||||
disks = topology.GetNodeDisks("non-existent")
|
||||
assert.Nil(t, disks)
|
||||
}
|
||||
|
||||
// Helper functions to create test topologies
|
||||
|
||||
func createSampleTopology() *master_pb.TopologyInfo {
|
||||
return &master_pb.TopologyInfo{
|
||||
DataCenterInfos: []*master_pb.DataCenterInfo{
|
||||
{
|
||||
Id: "dc1",
|
||||
RackInfos: []*master_pb.RackInfo{
|
||||
{
|
||||
Id: "rack1",
|
||||
DataNodeInfos: []*master_pb.DataNodeInfo{
|
||||
{
|
||||
Id: "10.0.0.1:8080",
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"hdd": {DiskId: 0, VolumeCount: 10, MaxVolumeCount: 100},
|
||||
"ssd": {DiskId: 1, VolumeCount: 5, MaxVolumeCount: 50},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "10.0.0.2:8080",
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"hdd": {DiskId: 0, VolumeCount: 8, MaxVolumeCount: 100},
|
||||
"ssd": {DiskId: 1, VolumeCount: 3, MaxVolumeCount: 50},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createEmptyTopology() *master_pb.TopologyInfo {
|
||||
return &master_pb.TopologyInfo{
|
||||
DataCenterInfos: []*master_pb.DataCenterInfo{
|
||||
{
|
||||
Id: "dc1",
|
||||
RackInfos: []*master_pb.RackInfo{
|
||||
{
|
||||
Id: "rack1",
|
||||
DataNodeInfos: []*master_pb.DataNodeInfo{
|
||||
{
|
||||
Id: "10.0.0.1:8080",
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"hdd": {DiskId: 0, VolumeCount: 0, MaxVolumeCount: 100},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createUnbalancedTopology() *master_pb.TopologyInfo {
|
||||
return &master_pb.TopologyInfo{
|
||||
DataCenterInfos: []*master_pb.DataCenterInfo{
|
||||
{
|
||||
Id: "dc1",
|
||||
RackInfos: []*master_pb.RackInfo{
|
||||
{
|
||||
Id: "rack1",
|
||||
DataNodeInfos: []*master_pb.DataNodeInfo{
|
||||
{
|
||||
Id: "10.0.0.1:8080",
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"hdd": {DiskId: 0, VolumeCount: 90, MaxVolumeCount: 100}, // Very loaded
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "10.0.0.2:8080",
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"hdd": {DiskId: 0, VolumeCount: 10, MaxVolumeCount: 100}, // Lightly loaded
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createHighGarbageTopology() *master_pb.TopologyInfo {
|
||||
// In a real implementation, this would include volume-level garbage metrics
|
||||
return createSampleTopology()
|
||||
}
|
||||
|
||||
func createLargeVolumeTopology() *master_pb.TopologyInfo {
|
||||
// In a real implementation, this would include volume-level size metrics
|
||||
return createSampleTopology()
|
||||
}
|
||||
|
||||
func createTopologyWithLoad() *ActiveTopology {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createSampleTopology())
|
||||
|
||||
// Add some existing tasks to create load
|
||||
err := topology.AddPendingTask(TaskSpec{
|
||||
TaskID: "existing1",
|
||||
TaskType: TaskTypeVacuum,
|
||||
VolumeID: 2001,
|
||||
VolumeSize: 0,
|
||||
Sources: []TaskSourceSpec{
|
||||
{ServerID: "10.0.0.1:8080", DiskID: 0},
|
||||
},
|
||||
Destinations: []TaskDestinationSpec{
|
||||
{ServerID: "", DiskID: 0}, // Vacuum doesn't have a destination
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// In test helper function, just log error instead of failing
|
||||
fmt.Printf("Warning: Failed to add existing task: %v\n", err)
|
||||
}
|
||||
topology.AssignTask("existing1")
|
||||
|
||||
return topology
|
||||
}
|
||||
|
||||
func createTopologyForEC() *ActiveTopology {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createSampleTopology())
|
||||
return topology
|
||||
}
|
||||
|
||||
func createTopologyWithConflicts() *ActiveTopology {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createSampleTopology())
|
||||
|
||||
// Add conflicting tasks
|
||||
err := topology.AddPendingTask(TaskSpec{
|
||||
TaskID: "balance1",
|
||||
TaskType: TaskTypeBalance,
|
||||
VolumeID: 3001,
|
||||
VolumeSize: 1024 * 1024 * 1024,
|
||||
Sources: []TaskSourceSpec{
|
||||
{ServerID: "10.0.0.1:8080", DiskID: 0},
|
||||
},
|
||||
Destinations: []TaskDestinationSpec{
|
||||
{ServerID: "10.0.0.2:8080", DiskID: 0},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to add balance task: %v\n", err)
|
||||
}
|
||||
topology.AssignTask("balance1")
|
||||
|
||||
err = topology.AddPendingTask(TaskSpec{
|
||||
TaskID: "ec1",
|
||||
TaskType: TaskTypeErasureCoding,
|
||||
VolumeID: 3002,
|
||||
VolumeSize: 1024 * 1024 * 1024,
|
||||
Sources: []TaskSourceSpec{
|
||||
{ServerID: "10.0.0.1:8080", DiskID: 1},
|
||||
},
|
||||
Destinations: []TaskDestinationSpec{
|
||||
{ServerID: "", DiskID: 0}, // EC doesn't have single destination
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to add EC task: %v\n", err)
|
||||
}
|
||||
topology.AssignTask("ec1")
|
||||
|
||||
return topology
|
||||
}
|
||||
|
||||
// TestDestinationPlanning tests that the public interface works correctly
|
||||
// NOTE: Destination planning is now done in task detection phase, not in ActiveTopology
|
||||
func TestDestinationPlanning(t *testing.T) {
|
||||
topology := NewActiveTopology(10)
|
||||
topology.UpdateTopology(createSampleTopology())
|
||||
|
||||
// Test that GetAvailableDisks works for destination planning
|
||||
t.Run("GetAvailableDisks functionality", func(t *testing.T) {
|
||||
availableDisks := topology.GetAvailableDisks(TaskTypeBalance, "10.0.0.1:8080")
|
||||
assert.Greater(t, len(availableDisks), 0)
|
||||
|
||||
// Should exclude the source node
|
||||
for _, disk := range availableDisks {
|
||||
assert.NotEqual(t, "10.0.0.1:8080", disk.NodeID)
|
||||
}
|
||||
})
|
||||
|
||||
// Test that topology state can be used for planning
|
||||
t.Run("Topology provides planning information", func(t *testing.T) {
|
||||
topologyInfo := topology.GetTopologyInfo()
|
||||
assert.NotNil(t, topologyInfo)
|
||||
assert.Greater(t, len(topologyInfo.DataCenterInfos), 0)
|
||||
|
||||
// Test getting node disks
|
||||
disks := topology.GetNodeDisks("10.0.0.1:8080")
|
||||
assert.Greater(t, len(disks), 0)
|
||||
})
|
||||
}
|
||||
389
weed/admin/topology/capacity.go
Normal file
389
weed/admin/topology/capacity.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
)
|
||||
|
||||
// GetEffectiveAvailableCapacity returns the effective available capacity for a disk
|
||||
// This considers BOTH pending and assigned tasks for capacity reservation.
|
||||
//
|
||||
// Formula: BaseAvailable - (VolumeSlots + ShardSlots/ShardsPerVolumeSlot) from all tasks
|
||||
//
|
||||
// The calculation includes:
|
||||
// - Pending tasks: Reserve capacity immediately when added
|
||||
// - Assigned tasks: Continue to reserve capacity during execution
|
||||
// - Recently completed tasks are NOT counted against capacity
|
||||
func (at *ActiveTopology) GetEffectiveAvailableCapacity(nodeID string, diskID uint32) int64 {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
diskKey := fmt.Sprintf("%s:%d", nodeID, diskID)
|
||||
disk, exists := at.disks[diskKey]
|
||||
if !exists {
|
||||
return 0
|
||||
}
|
||||
|
||||
if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Use the same logic as getEffectiveAvailableCapacityUnsafe but with locking
|
||||
capacity := at.getEffectiveAvailableCapacityUnsafe(disk)
|
||||
return int64(capacity.VolumeSlots)
|
||||
}
|
||||
|
||||
// GetEffectiveAvailableCapacityDetailed returns detailed available capacity as StorageSlotChange
|
||||
// This provides granular information about available volume slots and shard slots
|
||||
func (at *ActiveTopology) GetEffectiveAvailableCapacityDetailed(nodeID string, diskID uint32) StorageSlotChange {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
diskKey := fmt.Sprintf("%s:%d", nodeID, diskID)
|
||||
disk, exists := at.disks[diskKey]
|
||||
if !exists {
|
||||
return StorageSlotChange{}
|
||||
}
|
||||
|
||||
if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
|
||||
return StorageSlotChange{}
|
||||
}
|
||||
|
||||
return at.getEffectiveAvailableCapacityUnsafe(disk)
|
||||
}
|
||||
|
||||
// GetEffectiveAvailableEcShardSlots returns a disk's free EC shard slots,
|
||||
// accounting for in-flight task reservations at shard granularity. Unlike the
|
||||
// volume-slot views (GetDisksWithEffectiveCapacity / GetEffectiveAvailableCapacity),
|
||||
// this does not truncate sub-volume shard reservations: it subtracts the full
|
||||
// reservation impact (volume slots converted to shard slots, plus the raw shard
|
||||
// slots) so a reservation that is not a whole multiple of ShardsPerVolumeSlot is
|
||||
// not lost. It does NOT subtract the EC shards already persisted on the disk;
|
||||
// callers that track those (from EcShardInfos) subtract them separately.
|
||||
//
|
||||
// shardsPerVolume is the number of EC shards of the target collection that fit in
|
||||
// one volume slot (i.e. its data-shard count): a 4+2 volume's shards are ~1/4 of a
|
||||
// volume each, so one volume slot holds 4 of them, not the default
|
||||
// ShardsPerVolumeSlot. Pass <= 0 to use the default. Using the target ratio keeps
|
||||
// Place from over-filling a disk for low-data-shard layouts.
|
||||
func (at *ActiveTopology) GetEffectiveAvailableEcShardSlots(nodeID string, diskID uint32, shardsPerVolume int) int {
|
||||
if shardsPerVolume <= 0 {
|
||||
shardsPerVolume = ShardsPerVolumeSlot
|
||||
}
|
||||
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
diskKey := fmt.Sprintf("%s:%d", nodeID, diskID)
|
||||
disk, exists := at.disks[diskKey]
|
||||
if !exists || disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
info := disk.DiskInfo.DiskInfo
|
||||
base := info.MaxVolumeCount - info.VolumeCount
|
||||
if base <= 0 && info.MaxVolumeCount == 0 && info.VolumeCount == 0 &&
|
||||
len(info.VolumeInfos) == 0 && len(info.EcShardInfos) == 0 {
|
||||
// Freshly started empty servers can report max=0 before publishing concrete
|
||||
// limits; keep one provisional slot so EC placement still sees the disk,
|
||||
// mirroring getEffectiveAvailableCapacityUnsafe.
|
||||
base = 1
|
||||
}
|
||||
if base < 0 {
|
||||
base = 0
|
||||
}
|
||||
// calculateTaskStorageImpact reports consumption as positive, so subtract it.
|
||||
// Volume-slot reservations scale by the target ratio; the sub-volume shard-slot
|
||||
// remainder is in default units and subtracted as-is (a small approximation).
|
||||
impact := at.getEffectiveCapacityUnsafe(disk)
|
||||
// impact.ShardSlots is recorded in default ShardsPerVolumeSlot units; convert it
|
||||
// to the target ratio's shard slots before subtracting (identity when
|
||||
// shardsPerVolume == ShardsPerVolumeSlot). Round a positive reservation up so a
|
||||
// sub-slot reservation (e.g. 1 default slot against a 4-shard target) is not
|
||||
// truncated to zero and wrongly counted as free.
|
||||
scaledShardImpact := int64(impact.ShardSlots) * int64(shardsPerVolume)
|
||||
if scaledShardImpact > 0 {
|
||||
scaledShardImpact = (scaledShardImpact + int64(ShardsPerVolumeSlot) - 1) / int64(ShardsPerVolumeSlot)
|
||||
} else {
|
||||
scaledShardImpact /= int64(ShardsPerVolumeSlot)
|
||||
}
|
||||
free := base*int64(shardsPerVolume) -
|
||||
int64(impact.VolumeSlots)*int64(shardsPerVolume) -
|
||||
scaledShardImpact
|
||||
if free < 0 {
|
||||
free = 0
|
||||
}
|
||||
return int(free)
|
||||
}
|
||||
|
||||
// GetEffectiveCapacityImpact returns the StorageSlotChange impact for a disk
|
||||
// This shows the net impact from all pending and assigned tasks
|
||||
func (at *ActiveTopology) GetEffectiveCapacityImpact(nodeID string, diskID uint32) StorageSlotChange {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
diskKey := fmt.Sprintf("%s:%d", nodeID, diskID)
|
||||
disk, exists := at.disks[diskKey]
|
||||
if !exists {
|
||||
return StorageSlotChange{}
|
||||
}
|
||||
|
||||
return at.getEffectiveCapacityUnsafe(disk)
|
||||
}
|
||||
|
||||
// GetDisksWithEffectiveCapacity returns disks with sufficient effective capacity
|
||||
// This method considers BOTH pending and assigned tasks for capacity reservation using StorageSlotChange.
|
||||
//
|
||||
// Parameters:
|
||||
// - taskType: type of task to check compatibility for
|
||||
// - excludeNodeID: node to exclude from results
|
||||
// - minCapacity: minimum effective capacity required (in volume slots)
|
||||
//
|
||||
// Returns: DiskInfo objects where VolumeCount reflects capacity reserved by all tasks
|
||||
func (at *ActiveTopology) GetDisksWithEffectiveCapacity(taskType TaskType, excludeNodeID string, minCapacity int64) []*DiskInfo {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
var available []*DiskInfo
|
||||
|
||||
glog.V(2).Infof("GetDisksWithEffectiveCapacity checking %d disks for type %s, minCapacity %d", len(at.disks), taskType, minCapacity)
|
||||
for _, disk := range at.disks {
|
||||
if disk.NodeID == excludeNodeID {
|
||||
continue // Skip excluded node
|
||||
}
|
||||
|
||||
if at.isDiskAvailable(disk, taskType) {
|
||||
effectiveCapacity := at.getEffectiveAvailableCapacityUnsafe(disk)
|
||||
|
||||
// Only include disks that meet minimum capacity requirement
|
||||
if int64(effectiveCapacity.VolumeSlots) >= minCapacity {
|
||||
// Create a new DiskInfo with current capacity information
|
||||
diskCopy := DiskInfo{
|
||||
NodeID: disk.DiskInfo.NodeID,
|
||||
Address: disk.DiskInfo.Address,
|
||||
DiskID: disk.DiskInfo.DiskID,
|
||||
DiskType: disk.DiskInfo.DiskType,
|
||||
DataCenter: disk.DiskInfo.DataCenter,
|
||||
Rack: disk.DiskInfo.Rack,
|
||||
LoadCount: len(disk.pendingTasks) + len(disk.assignedTasks), // Count all tasks
|
||||
}
|
||||
|
||||
// Create a new protobuf DiskInfo to avoid modifying the original
|
||||
diskInfoCopy := &master_pb.DiskInfo{
|
||||
DiskId: disk.DiskInfo.DiskInfo.DiskId,
|
||||
MaxVolumeCount: disk.DiskInfo.DiskInfo.MaxVolumeCount,
|
||||
VolumeCount: disk.DiskInfo.DiskInfo.MaxVolumeCount - int64(effectiveCapacity.VolumeSlots),
|
||||
VolumeInfos: disk.DiskInfo.DiskInfo.VolumeInfos,
|
||||
EcShardInfos: disk.DiskInfo.DiskInfo.EcShardInfos,
|
||||
RemoteVolumeCount: disk.DiskInfo.DiskInfo.RemoteVolumeCount,
|
||||
ActiveVolumeCount: disk.DiskInfo.DiskInfo.ActiveVolumeCount,
|
||||
FreeVolumeCount: disk.DiskInfo.DiskInfo.FreeVolumeCount,
|
||||
Tags: append([]string(nil), disk.DiskInfo.DiskInfo.Tags...),
|
||||
DiskTotalBytes: disk.DiskInfo.DiskInfo.DiskTotalBytes,
|
||||
DiskFreeBytes: disk.DiskInfo.DiskInfo.DiskFreeBytes,
|
||||
}
|
||||
diskCopy.DiskInfo = diskInfoCopy
|
||||
diskCopy.DiskInfo.MaxVolumeCount = disk.DiskInfo.DiskInfo.MaxVolumeCount // Ensure Max is set
|
||||
|
||||
available = append(available, &diskCopy)
|
||||
} else {
|
||||
glog.V(2).Infof("Disk %s:%d capacity %d < %d (Max:%d, Vol:%d)", disk.NodeID, disk.DiskInfo.DiskID, effectiveCapacity.VolumeSlots, minCapacity, disk.DiskInfo.DiskInfo.MaxVolumeCount, disk.DiskInfo.DiskInfo.VolumeCount)
|
||||
}
|
||||
} else {
|
||||
tasksInfo := ""
|
||||
for _, t := range disk.pendingTasks {
|
||||
tasksInfo += fmt.Sprintf("[P:%s,Vol:%d] ", t.TaskType, t.VolumeID)
|
||||
}
|
||||
for _, t := range disk.assignedTasks {
|
||||
tasksInfo += fmt.Sprintf("[A:%s,Vol:%d] ", t.TaskType, t.VolumeID)
|
||||
}
|
||||
glog.V(2).Infof("Disk %s:%d unavailable. Load: %d, MaxLoad: %d. Tasks: %s", disk.NodeID, disk.DiskInfo.DiskID, len(disk.pendingTasks)+len(disk.assignedTasks), MaxConcurrentTasksPerDisk, tasksInfo)
|
||||
}
|
||||
}
|
||||
glog.V(2).Infof("GetDisksWithEffectiveCapacity found %d available disks", len(available))
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
// GetDisksForPlanning returns disks considering both active and pending tasks for planning decisions
|
||||
// This helps avoid over-scheduling tasks to the same disk
|
||||
func (at *ActiveTopology) GetDisksForPlanning(taskType TaskType, excludeNodeID string, minCapacity int64) []*DiskInfo {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
var available []*DiskInfo
|
||||
|
||||
for _, disk := range at.disks {
|
||||
if disk.NodeID == excludeNodeID {
|
||||
continue // Skip excluded node
|
||||
}
|
||||
|
||||
// Consider both pending and active tasks for scheduling decisions
|
||||
if at.isDiskAvailableForPlanning(disk, taskType) {
|
||||
// Check if disk can accommodate new task considering pending tasks
|
||||
planningCapacity := at.getPlanningCapacityUnsafe(disk)
|
||||
|
||||
if int64(planningCapacity.VolumeSlots) >= minCapacity {
|
||||
// Create a new DiskInfo with planning information
|
||||
diskCopy := DiskInfo{
|
||||
NodeID: disk.DiskInfo.NodeID,
|
||||
Address: disk.DiskInfo.Address,
|
||||
DiskID: disk.DiskInfo.DiskID,
|
||||
DiskType: disk.DiskInfo.DiskType,
|
||||
DataCenter: disk.DiskInfo.DataCenter,
|
||||
Rack: disk.DiskInfo.Rack,
|
||||
LoadCount: len(disk.pendingTasks) + len(disk.assignedTasks),
|
||||
}
|
||||
|
||||
// Create a new protobuf DiskInfo to avoid modifying the original
|
||||
diskInfoCopy := &master_pb.DiskInfo{
|
||||
DiskId: disk.DiskInfo.DiskInfo.DiskId,
|
||||
MaxVolumeCount: disk.DiskInfo.DiskInfo.MaxVolumeCount,
|
||||
VolumeCount: disk.DiskInfo.DiskInfo.MaxVolumeCount - int64(planningCapacity.VolumeSlots),
|
||||
VolumeInfos: disk.DiskInfo.DiskInfo.VolumeInfos,
|
||||
EcShardInfos: disk.DiskInfo.DiskInfo.EcShardInfos,
|
||||
RemoteVolumeCount: disk.DiskInfo.DiskInfo.RemoteVolumeCount,
|
||||
ActiveVolumeCount: disk.DiskInfo.DiskInfo.ActiveVolumeCount,
|
||||
FreeVolumeCount: disk.DiskInfo.DiskInfo.FreeVolumeCount,
|
||||
Tags: append([]string(nil), disk.DiskInfo.DiskInfo.Tags...),
|
||||
DiskTotalBytes: disk.DiskInfo.DiskInfo.DiskTotalBytes,
|
||||
DiskFreeBytes: disk.DiskInfo.DiskInfo.DiskFreeBytes,
|
||||
}
|
||||
diskCopy.DiskInfo = diskInfoCopy
|
||||
|
||||
available = append(available, &diskCopy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
// CanAccommodateTask checks if a disk can accommodate a new task considering all constraints
|
||||
func (at *ActiveTopology) CanAccommodateTask(nodeID string, diskID uint32, taskType TaskType, volumesNeeded int64) bool {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
diskKey := fmt.Sprintf("%s:%d", nodeID, diskID)
|
||||
disk, exists := at.disks[diskKey]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check basic availability
|
||||
if !at.isDiskAvailable(disk, taskType) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check effective capacity
|
||||
effectiveCapacity := at.getEffectiveAvailableCapacityUnsafe(disk)
|
||||
return int64(effectiveCapacity.VolumeSlots) >= volumesNeeded
|
||||
}
|
||||
|
||||
// getPlanningCapacityUnsafe considers both pending and active tasks for planning
|
||||
func (at *ActiveTopology) getPlanningCapacityUnsafe(disk *activeDisk) StorageSlotChange {
|
||||
if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
|
||||
return StorageSlotChange{}
|
||||
}
|
||||
|
||||
baseAvailableVolumes := disk.DiskInfo.DiskInfo.MaxVolumeCount - disk.DiskInfo.DiskInfo.VolumeCount
|
||||
|
||||
// Use the centralized helper function to calculate task storage impact
|
||||
totalImpact := at.calculateTaskStorageImpact(disk)
|
||||
|
||||
// Calculate available capacity considering impact (negative impact reduces availability)
|
||||
availableVolumeSlots := baseAvailableVolumes - totalImpact.ToVolumeSlots()
|
||||
if availableVolumeSlots < 0 {
|
||||
availableVolumeSlots = 0
|
||||
}
|
||||
|
||||
// Return detailed capacity information
|
||||
return StorageSlotChange{
|
||||
VolumeSlots: int32(availableVolumeSlots),
|
||||
ShardSlots: -totalImpact.ShardSlots, // Available shard capacity (negative impact becomes positive availability)
|
||||
}
|
||||
}
|
||||
|
||||
// isDiskAvailableForPlanning checks if disk can accept new tasks considering
|
||||
// pending load. See isDiskAvailable for the cross-type policy.
|
||||
func (at *ActiveTopology) isDiskAvailableForPlanning(disk *activeDisk, taskType TaskType) bool {
|
||||
totalLoad := len(disk.pendingTasks) + len(disk.assignedTasks)
|
||||
if MaxTotalTaskLoadPerDisk > 0 && totalLoad >= MaxTotalTaskLoadPerDisk {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// calculateTaskStorageImpact is a helper function that calculates the total storage impact
|
||||
// from all tasks (pending and assigned) on a given disk. This eliminates code duplication
|
||||
// between multiple capacity calculation functions.
|
||||
func (at *ActiveTopology) calculateTaskStorageImpact(disk *activeDisk) StorageSlotChange {
|
||||
if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
|
||||
return StorageSlotChange{}
|
||||
}
|
||||
|
||||
totalImpact := StorageSlotChange{}
|
||||
|
||||
// Process both pending and assigned tasks with identical logic
|
||||
taskLists := [][]*taskState{disk.pendingTasks, disk.assignedTasks}
|
||||
|
||||
for _, taskList := range taskLists {
|
||||
for _, task := range taskList {
|
||||
// Calculate impact for all source locations
|
||||
for _, source := range task.Sources {
|
||||
if source.SourceServer == disk.NodeID && source.SourceDisk == disk.DiskID {
|
||||
totalImpact.AddInPlace(source.StorageChange)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate impact for all destination locations
|
||||
for _, dest := range task.Destinations {
|
||||
if dest.TargetServer == disk.NodeID && dest.TargetDisk == disk.DiskID {
|
||||
totalImpact.AddInPlace(dest.StorageChange)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return totalImpact
|
||||
}
|
||||
|
||||
// getEffectiveCapacityUnsafe returns effective capacity impact without locking (for internal use)
|
||||
// Returns StorageSlotChange representing the net impact from all tasks
|
||||
func (at *ActiveTopology) getEffectiveCapacityUnsafe(disk *activeDisk) StorageSlotChange {
|
||||
return at.calculateTaskStorageImpact(disk)
|
||||
}
|
||||
|
||||
// getEffectiveAvailableCapacityUnsafe returns detailed available capacity as StorageSlotChange
|
||||
func (at *ActiveTopology) getEffectiveAvailableCapacityUnsafe(disk *activeDisk) StorageSlotChange {
|
||||
if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
|
||||
return StorageSlotChange{}
|
||||
}
|
||||
|
||||
baseAvailable := disk.DiskInfo.DiskInfo.MaxVolumeCount - disk.DiskInfo.DiskInfo.VolumeCount
|
||||
if baseAvailable <= 0 &&
|
||||
disk.DiskInfo.DiskInfo.MaxVolumeCount == 0 &&
|
||||
disk.DiskInfo.DiskInfo.VolumeCount == 0 &&
|
||||
len(disk.DiskInfo.DiskInfo.VolumeInfos) == 0 &&
|
||||
len(disk.DiskInfo.DiskInfo.EcShardInfos) == 0 {
|
||||
// Some empty volume servers can report max_volume_counts=0 before
|
||||
// publishing concrete slot limits. Keep one provisional slot so EC
|
||||
// detection still sees the disk for placement planning.
|
||||
baseAvailable = 1
|
||||
}
|
||||
netImpact := at.getEffectiveCapacityUnsafe(disk)
|
||||
|
||||
// Calculate available volume slots (negative impact reduces availability)
|
||||
availableVolumeSlots := baseAvailable - netImpact.ToVolumeSlots()
|
||||
if availableVolumeSlots < 0 {
|
||||
availableVolumeSlots = 0
|
||||
}
|
||||
|
||||
// Return detailed capacity information
|
||||
return StorageSlotChange{
|
||||
VolumeSlots: int32(availableVolumeSlots),
|
||||
ShardSlots: -netImpact.ShardSlots, // Available shard capacity (negative impact becomes positive availability)
|
||||
}
|
||||
}
|
||||
82
weed/admin/topology/capacity_limits_test.go
Normal file
82
weed/admin/topology/capacity_limits_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
)
|
||||
|
||||
func TestGetDisksWithEffectiveCapacityNotCappedAtTenByLoad(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
activeTopology := NewActiveTopology(0)
|
||||
if err := activeTopology.UpdateTopology(singleDiskTopologyInfoForCapacityTest()); err != nil {
|
||||
t.Fatalf("UpdateTopology: %v", err)
|
||||
}
|
||||
|
||||
const pendingTasks = 32
|
||||
for i := 0; i < pendingTasks; i++ {
|
||||
taskID := fmt.Sprintf("ec-capacity-%d", i)
|
||||
err := activeTopology.AddPendingTask(TaskSpec{
|
||||
TaskID: taskID,
|
||||
TaskType: TaskTypeErasureCoding,
|
||||
VolumeID: uint32(i + 1),
|
||||
VolumeSize: 1,
|
||||
Sources: []TaskSourceSpec{
|
||||
{
|
||||
ServerID: "node-a",
|
||||
DiskID: 0,
|
||||
StorageImpact: &StorageSlotChange{},
|
||||
},
|
||||
},
|
||||
Destinations: []TaskDestinationSpec{
|
||||
{
|
||||
ServerID: "node-a",
|
||||
DiskID: 0,
|
||||
StorageImpact: &StorageSlotChange{},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("AddPendingTask(%s): %v", taskID, err)
|
||||
}
|
||||
}
|
||||
|
||||
disks := activeTopology.GetDisksWithEffectiveCapacity(TaskTypeErasureCoding, "", 1)
|
||||
if len(disks) != 1 {
|
||||
t.Fatalf("expected disk to remain available after %d pending tasks, got %d", pendingTasks, len(disks))
|
||||
}
|
||||
if disks[0].LoadCount != pendingTasks {
|
||||
t.Fatalf("unexpected load count: got=%d want=%d", disks[0].LoadCount, pendingTasks)
|
||||
}
|
||||
}
|
||||
|
||||
func singleDiskTopologyInfoForCapacityTest() *master_pb.TopologyInfo {
|
||||
return &master_pb.TopologyInfo{
|
||||
Id: "topology-test",
|
||||
DataCenterInfos: []*master_pb.DataCenterInfo{
|
||||
{
|
||||
Id: "dc1",
|
||||
RackInfos: []*master_pb.RackInfo{
|
||||
{
|
||||
Id: "rack1",
|
||||
DataNodeInfos: []*master_pb.DataNodeInfo{
|
||||
{
|
||||
Id: "node-a",
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"hdd": {
|
||||
DiskId: 0,
|
||||
Type: "hdd",
|
||||
VolumeCount: 0,
|
||||
MaxVolumeCount: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
54
weed/admin/topology/count_topology_resources_test.go
Normal file
54
weed/admin/topology/count_topology_resources_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
)
|
||||
|
||||
// TestCountTopologyResources_multiDiskPerNode covers the case where the master
|
||||
// keys DiskInfos by disk type, so several same-type physical disks on a node
|
||||
// collapse into a single DiskInfo entry. Counting len(DiskInfos) under-reports
|
||||
// the physical disk count and disagrees with the per-disk activeDisk map that
|
||||
// the rest of the admin topology builds via SplitByPhysicalDisk.
|
||||
func TestCountTopologyResources_multiDiskPerNode(t *testing.T) {
|
||||
makeNode := func(id string) *master_pb.DataNodeInfo {
|
||||
var ecShardInfos []*master_pb.VolumeEcShardInformationMessage
|
||||
for diskId := uint32(0); diskId < 6; diskId++ {
|
||||
ecShardInfos = append(ecShardInfos, &master_pb.VolumeEcShardInformationMessage{
|
||||
Id: diskId + 1,
|
||||
DiskId: diskId,
|
||||
EcIndexBits: 1,
|
||||
})
|
||||
}
|
||||
return &master_pb.DataNodeInfo{
|
||||
Id: id,
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"": {Type: "", MaxVolumeCount: 60, EcShardInfos: ecShardInfos},
|
||||
},
|
||||
}
|
||||
}
|
||||
topo := &master_pb.TopologyInfo{
|
||||
Id: "multi_disk_topo",
|
||||
DataCenterInfos: []*master_pb.DataCenterInfo{{
|
||||
Id: "dc1",
|
||||
RackInfos: []*master_pb.RackInfo{{
|
||||
Id: "rack1",
|
||||
DataNodeInfos: []*master_pb.DataNodeInfo{
|
||||
makeNode("node1"), makeNode("node2"), makeNode("node3"),
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
dcCount, nodeCount, diskCount := CountTopologyResources(topo)
|
||||
if dcCount != 1 {
|
||||
t.Errorf("dcCount = %d, want 1", dcCount)
|
||||
}
|
||||
if nodeCount != 3 {
|
||||
t.Errorf("nodeCount = %d, want 3", nodeCount)
|
||||
}
|
||||
if diskCount != 18 {
|
||||
t.Errorf("diskCount = %d, want 18 (6 physical disks x 3 nodes)", diskCount)
|
||||
}
|
||||
}
|
||||
87
weed/admin/topology/internal.go
Normal file
87
weed/admin/topology/internal.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// reassignTaskStates assigns tasks to the appropriate disks
|
||||
func (at *ActiveTopology) reassignTaskStates() {
|
||||
// Clear existing task assignments
|
||||
for _, disk := range at.disks {
|
||||
disk.pendingTasks = nil
|
||||
disk.assignedTasks = nil
|
||||
disk.recentTasks = nil
|
||||
}
|
||||
|
||||
// Reassign pending tasks
|
||||
for _, task := range at.pendingTasks {
|
||||
at.assignTaskToDisk(task)
|
||||
}
|
||||
|
||||
// Reassign assigned tasks
|
||||
for _, task := range at.assignedTasks {
|
||||
at.assignTaskToDisk(task)
|
||||
}
|
||||
|
||||
// Reassign recent tasks
|
||||
for _, task := range at.recentTasks {
|
||||
at.assignTaskToDisk(task)
|
||||
}
|
||||
}
|
||||
|
||||
// assignTaskToDisk assigns a task to the appropriate disk(s)
|
||||
func (at *ActiveTopology) assignTaskToDisk(task *taskState) {
|
||||
addedDisks := make(map[string]bool)
|
||||
|
||||
// Local helper function to assign task to a disk and avoid code duplication
|
||||
assign := func(server string, diskID uint32) {
|
||||
key := fmt.Sprintf("%s:%d", server, diskID)
|
||||
if server == "" || addedDisks[key] {
|
||||
return
|
||||
}
|
||||
if disk, exists := at.disks[key]; exists {
|
||||
switch task.Status {
|
||||
case TaskStatusPending:
|
||||
disk.pendingTasks = append(disk.pendingTasks, task)
|
||||
case TaskStatusInProgress:
|
||||
disk.assignedTasks = append(disk.assignedTasks, task)
|
||||
case TaskStatusCompleted:
|
||||
disk.recentTasks = append(disk.recentTasks, task)
|
||||
}
|
||||
addedDisks[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Assign to all source disks
|
||||
for _, source := range task.Sources {
|
||||
assign(source.SourceServer, source.SourceDisk)
|
||||
}
|
||||
|
||||
// Assign to all destination disks (duplicates automatically avoided by helper)
|
||||
for _, dest := range task.Destinations {
|
||||
assign(dest.TargetServer, dest.TargetDisk)
|
||||
}
|
||||
}
|
||||
|
||||
// isDiskAvailable checks if a disk can accept new tasks. Per-volume safety is
|
||||
// enforced by HasAnyTask at detection time, so cross-type tasks on the same
|
||||
// disk are intentionally not considered conflicting (see #9147).
|
||||
func (at *ActiveTopology) isDiskAvailable(disk *activeDisk, taskType TaskType) bool {
|
||||
activeLoad := len(disk.pendingTasks) + len(disk.assignedTasks)
|
||||
if MaxConcurrentTasksPerDisk > 0 && activeLoad >= MaxConcurrentTasksPerDisk {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// cleanupRecentTasks removes old recent tasks
|
||||
func (at *ActiveTopology) cleanupRecentTasks() {
|
||||
cutoff := time.Now().Add(-time.Duration(at.recentTaskWindowSeconds) * time.Second)
|
||||
|
||||
for taskID, task := range at.recentTasks {
|
||||
if task.CompletedAt.Before(cutoff) {
|
||||
delete(at.recentTasks, taskID)
|
||||
}
|
||||
}
|
||||
}
|
||||
55
weed/admin/topology/storage_impact.go
Normal file
55
weed/admin/topology/storage_impact.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
||||
)
|
||||
|
||||
// CalculateTaskStorageImpact calculates storage impact for different task types
|
||||
func CalculateTaskStorageImpact(taskType TaskType, volumeSize int64) (sourceChange, targetChange StorageSlotChange) {
|
||||
switch taskType {
|
||||
case TaskTypeErasureCoding:
|
||||
// EC task: distributes shards to MULTIPLE targets, source reserves with zero impact
|
||||
// Source reserves capacity but with zero StorageSlotChange (no actual capacity consumption during planning)
|
||||
// WARNING: EC has multiple targets! Use AddPendingTask with multiple destinations for proper multi-target handling
|
||||
// This simplified function returns zero impact; real EC requires specialized multi-destination calculation
|
||||
return StorageSlotChange{VolumeSlots: 0, ShardSlots: 0}, StorageSlotChange{VolumeSlots: 0, ShardSlots: 0}
|
||||
|
||||
case TaskTypeBalance:
|
||||
// Balance task: moves volume from source to target
|
||||
// Source loses 1 volume, target gains 1 volume
|
||||
return StorageSlotChange{VolumeSlots: -1, ShardSlots: 0}, StorageSlotChange{VolumeSlots: 1, ShardSlots: 0}
|
||||
|
||||
case TaskTypeVacuum:
|
||||
// Vacuum task: frees space by removing deleted entries, no slot change
|
||||
return StorageSlotChange{VolumeSlots: 0, ShardSlots: 0}, StorageSlotChange{VolumeSlots: 0, ShardSlots: 0}
|
||||
|
||||
case TaskTypeReplication:
|
||||
// Replication task: creates new replica on target
|
||||
return StorageSlotChange{VolumeSlots: 0, ShardSlots: 0}, StorageSlotChange{VolumeSlots: 1, ShardSlots: 0}
|
||||
|
||||
case TaskTypeECBalance:
|
||||
// EC balance task: moves individual shard from source to target
|
||||
// Source frees 1 shard slot, target gains 1 shard slot
|
||||
return StorageSlotChange{VolumeSlots: 0, ShardSlots: -1}, StorageSlotChange{VolumeSlots: 0, ShardSlots: 1}
|
||||
|
||||
default:
|
||||
// Unknown task type, assume minimal impact
|
||||
glog.Warningf("unhandled task type %s in CalculateTaskStorageImpact, assuming default impact", taskType)
|
||||
return StorageSlotChange{VolumeSlots: 0, ShardSlots: 0}, StorageSlotChange{VolumeSlots: 1, ShardSlots: 0}
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateECShardStorageImpact calculates storage impact for EC shards specifically
|
||||
func CalculateECShardStorageImpact(shardCount int32, expectedShardSize int64) StorageSlotChange {
|
||||
// EC shards are typically much smaller than full volumes
|
||||
// Use shard-level tracking for granular capacity planning
|
||||
return StorageSlotChange{VolumeSlots: 0, ShardSlots: shardCount}
|
||||
}
|
||||
|
||||
// CalculateECShardCleanupImpact calculates storage impact for cleaning up existing EC shards
|
||||
func CalculateECShardCleanupImpact(originalVolumeSize int64) StorageSlotChange {
|
||||
// Cleaning up existing EC shards frees shard slots
|
||||
// Use the actual EC configuration constants for accurate shard count
|
||||
return StorageSlotChange{VolumeSlots: 0, ShardSlots: -int32(erasure_coding.TotalShardsCount)} // Negative = freed capacity
|
||||
}
|
||||
1058
weed/admin/topology/storage_slot_test.go
Normal file
1058
weed/admin/topology/storage_slot_test.go
Normal file
File diff suppressed because it is too large
Load Diff
126
weed/admin/topology/structs.go
Normal file
126
weed/admin/topology/structs.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
)
|
||||
|
||||
// TaskSource represents a single source in a multi-source task (for replicated volume cleanup)
|
||||
type TaskSource struct {
|
||||
SourceServer string `json:"source_server"`
|
||||
SourceDisk uint32 `json:"source_disk"`
|
||||
StorageChange StorageSlotChange `json:"storage_change"` // Storage impact on this source
|
||||
EstimatedSize int64 `json:"estimated_size"` // Estimated size for this source
|
||||
}
|
||||
|
||||
// TaskDestination represents a single destination in a multi-destination task
|
||||
type TaskDestination struct {
|
||||
TargetServer string `json:"target_server"`
|
||||
TargetDisk uint32 `json:"target_disk"`
|
||||
StorageChange StorageSlotChange `json:"storage_change"` // Storage impact on this destination
|
||||
EstimatedSize int64 `json:"estimated_size"` // Estimated size for this destination
|
||||
}
|
||||
|
||||
// taskState represents the current state of tasks affecting the topology (internal)
|
||||
// Uses unified multi-source/multi-destination design:
|
||||
// - Single-source tasks (balance, vacuum, replication): 1 source, 1 destination
|
||||
// - Multi-source EC tasks (replicated volumes): N sources, M destinations
|
||||
type taskState struct {
|
||||
VolumeID uint32 `json:"volume_id"`
|
||||
TaskType TaskType `json:"task_type"`
|
||||
Status TaskStatus `json:"status"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
CompletedAt time.Time `json:"completed_at,omitempty"`
|
||||
EstimatedSize int64 `json:"estimated_size"` // Total estimated size of task
|
||||
|
||||
// Unified source and destination arrays (always used)
|
||||
Sources []TaskSource `json:"sources"` // Source locations (1+ for all task types)
|
||||
Destinations []TaskDestination `json:"destinations"` // Destination locations (1+ for all task types)
|
||||
}
|
||||
|
||||
// DiskInfo represents a disk with its current state and ongoing tasks (public for external access)
|
||||
type DiskInfo struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Address string `json:"address"` // volume server ip:port; NodeID may be an opaque id, so this is the source for the physical-machine host
|
||||
DiskID uint32 `json:"disk_id"`
|
||||
DiskType string `json:"disk_type"`
|
||||
DataCenter string `json:"data_center"`
|
||||
Rack string `json:"rack"`
|
||||
DiskInfo *master_pb.DiskInfo `json:"disk_info"`
|
||||
LoadCount int `json:"load_count"` // Number of active tasks
|
||||
}
|
||||
|
||||
// activeDisk represents internal disk state (private)
|
||||
type activeDisk struct {
|
||||
*DiskInfo
|
||||
pendingTasks []*taskState
|
||||
assignedTasks []*taskState
|
||||
recentTasks []*taskState // Completed in last N seconds
|
||||
}
|
||||
|
||||
// activeNode represents a node with its disks (private)
|
||||
type activeNode struct {
|
||||
nodeID string
|
||||
dataCenter string
|
||||
rack string
|
||||
nodeInfo *master_pb.DataNodeInfo
|
||||
disks map[uint32]*activeDisk // DiskID -> activeDisk
|
||||
}
|
||||
|
||||
// ActiveTopology provides a real-time view of cluster state with task awareness
|
||||
type ActiveTopology struct {
|
||||
// Core topology from master
|
||||
topologyInfo *master_pb.TopologyInfo
|
||||
lastUpdated time.Time
|
||||
|
||||
// Structured topology for easy access (private)
|
||||
nodes map[string]*activeNode // NodeID -> activeNode
|
||||
disks map[string]*activeDisk // "NodeID:DiskID" -> activeDisk
|
||||
|
||||
// Performance indexes for O(1) lookups (private)
|
||||
volumeIndex map[uint32][]string // VolumeID -> list of "NodeID:DiskID" where volume replicas exist
|
||||
ecShardIndex map[uint32][]string // VolumeID -> list of "NodeID:DiskID" where EC shards exist
|
||||
|
||||
// Task states affecting the topology (private)
|
||||
pendingTasks map[string]*taskState
|
||||
assignedTasks map[string]*taskState
|
||||
recentTasks map[string]*taskState
|
||||
|
||||
// Configuration
|
||||
recentTaskWindowSeconds int
|
||||
|
||||
// Synchronization
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// DestinationPlan represents a planned destination for a volume/shard operation
|
||||
type DestinationPlan struct {
|
||||
TargetNode string `json:"target_node"`
|
||||
TargetAddress string `json:"target_address"`
|
||||
TargetDisk uint32 `json:"target_disk"`
|
||||
TargetRack string `json:"target_rack"`
|
||||
TargetDC string `json:"target_dc"`
|
||||
ExpectedSize uint64 `json:"expected_size"`
|
||||
PlacementScore float64 `json:"placement_score"`
|
||||
}
|
||||
|
||||
// MultiDestinationPlan represents multiple planned destinations for operations like EC
|
||||
type MultiDestinationPlan struct {
|
||||
Plans []*DestinationPlan `json:"plans"`
|
||||
TotalShards int `json:"total_shards"`
|
||||
SuccessfulRack int `json:"successful_racks"`
|
||||
SuccessfulDCs int `json:"successful_dcs"`
|
||||
}
|
||||
|
||||
// VolumeReplica represents a replica location with server and disk information.
|
||||
// ShardIds is populated only by GetECShardLocations — it lists the EC shards
|
||||
// the disk holds for the volume.
|
||||
type VolumeReplica struct {
|
||||
ServerID string `json:"server_id"`
|
||||
DiskID uint32 `json:"disk_id"`
|
||||
DataCenter string `json:"data_center"`
|
||||
Rack string `json:"rack"`
|
||||
ShardIds []uint32 `json:"shard_ids,omitempty"`
|
||||
}
|
||||
365
weed/admin/topology/task_management.go
Normal file
365
weed/admin/topology/task_management.go
Normal file
@@ -0,0 +1,365 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
)
|
||||
|
||||
// AssignTask moves a task from pending to assigned and reserves capacity
|
||||
func (at *ActiveTopology) AssignTask(taskID string) error {
|
||||
at.mutex.Lock()
|
||||
defer at.mutex.Unlock()
|
||||
|
||||
task, exists := at.pendingTasks[taskID]
|
||||
if !exists {
|
||||
return fmt.Errorf("pending task %s not found", taskID)
|
||||
}
|
||||
|
||||
// Skip capacity check if topology hasn't been populated yet
|
||||
if len(at.disks) == 0 {
|
||||
glog.Warningf("AssignTask %s: topology has no disks yet, skipping capacity check", taskID)
|
||||
} else {
|
||||
// Check if all destination disks have sufficient capacity to reserve
|
||||
for _, dest := range task.Destinations {
|
||||
targetKey := fmt.Sprintf("%s:%d", dest.TargetServer, dest.TargetDisk)
|
||||
if targetDisk, exists := at.disks[targetKey]; exists {
|
||||
availableCapacity := at.getEffectiveAvailableCapacityUnsafe(targetDisk)
|
||||
|
||||
// Check if we have enough total capacity using the improved unified comparison
|
||||
if !availableCapacity.CanAccommodate(dest.StorageChange) {
|
||||
return fmt.Errorf("insufficient capacity on target disk %s:%d. Available: %+v, Required: %+v",
|
||||
dest.TargetServer, dest.TargetDisk, availableCapacity, dest.StorageChange)
|
||||
}
|
||||
} else if dest.TargetServer != "" {
|
||||
// Fail fast if destination disk is not found in topology
|
||||
var existingKeys []string
|
||||
for k := range at.disks {
|
||||
existingKeys = append(existingKeys, k)
|
||||
}
|
||||
glog.Warningf("destination disk %s not found in topology. Existing disk keys: %v", targetKey, existingKeys)
|
||||
return fmt.Errorf("destination disk %s not found in topology", targetKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move task to assigned and reserve capacity
|
||||
delete(at.pendingTasks, taskID)
|
||||
task.Status = TaskStatusInProgress
|
||||
at.assignedTasks[taskID] = task
|
||||
at.reassignTaskStates()
|
||||
|
||||
// Log capacity reservation information for all sources and destinations
|
||||
totalSourceImpact := StorageSlotChange{}
|
||||
totalDestImpact := StorageSlotChange{}
|
||||
for _, source := range task.Sources {
|
||||
totalSourceImpact.AddInPlace(source.StorageChange)
|
||||
}
|
||||
for _, dest := range task.Destinations {
|
||||
totalDestImpact.AddInPlace(dest.StorageChange)
|
||||
}
|
||||
|
||||
glog.V(2).Infof("Task %s assigned and capacity reserved: %d sources (VolumeSlots:%d, ShardSlots:%d), %d destinations (VolumeSlots:%d, ShardSlots:%d)",
|
||||
taskID, len(task.Sources), totalSourceImpact.VolumeSlots, totalSourceImpact.ShardSlots,
|
||||
len(task.Destinations), totalDestImpact.VolumeSlots, totalDestImpact.ShardSlots)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteTask moves a task from assigned to recent and releases reserved capacity
|
||||
// NOTE: This only releases the reserved capacity. The actual topology update (VolumeCount changes)
|
||||
// should be handled by the master when it receives the task completion notification.
|
||||
func (at *ActiveTopology) CompleteTask(taskID string) error {
|
||||
at.mutex.Lock()
|
||||
defer at.mutex.Unlock()
|
||||
|
||||
task, exists := at.assignedTasks[taskID]
|
||||
if !exists {
|
||||
// If not in assigned tasks, check pending tasks
|
||||
if task, exists = at.pendingTasks[taskID]; exists {
|
||||
delete(at.pendingTasks, taskID)
|
||||
} else {
|
||||
return fmt.Errorf("task %s not found in assigned or pending tasks", taskID)
|
||||
}
|
||||
} else {
|
||||
delete(at.assignedTasks, taskID)
|
||||
}
|
||||
|
||||
// Release reserved capacity by moving task to completed state
|
||||
task.Status = TaskStatusCompleted
|
||||
task.CompletedAt = time.Now()
|
||||
at.recentTasks[taskID] = task
|
||||
at.reassignTaskStates()
|
||||
|
||||
// Log capacity release information for all sources and destinations
|
||||
totalSourceImpact := StorageSlotChange{}
|
||||
totalDestImpact := StorageSlotChange{}
|
||||
for _, source := range task.Sources {
|
||||
totalSourceImpact.AddInPlace(source.StorageChange)
|
||||
}
|
||||
for _, dest := range task.Destinations {
|
||||
totalDestImpact.AddInPlace(dest.StorageChange)
|
||||
}
|
||||
|
||||
glog.V(2).Infof("Task %s completed and capacity released: %d sources (VolumeSlots:%d, ShardSlots:%d), %d destinations (VolumeSlots:%d, ShardSlots:%d)",
|
||||
taskID, len(task.Sources), totalSourceImpact.VolumeSlots, totalSourceImpact.ShardSlots,
|
||||
len(task.Destinations), totalDestImpact.VolumeSlots, totalDestImpact.ShardSlots)
|
||||
|
||||
// Clean up old recent tasks
|
||||
at.cleanupRecentTasks()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyActualStorageChange updates the topology to reflect actual storage changes after task completion
|
||||
// This should be called when the master updates the topology with new VolumeCount information
|
||||
func (at *ActiveTopology) ApplyActualStorageChange(nodeID string, diskID uint32, volumeCountChange int64) {
|
||||
at.mutex.Lock()
|
||||
defer at.mutex.Unlock()
|
||||
|
||||
diskKey := fmt.Sprintf("%s:%d", nodeID, diskID)
|
||||
if disk, exists := at.disks[diskKey]; exists && disk.DiskInfo != nil && disk.DiskInfo.DiskInfo != nil {
|
||||
oldCount := disk.DiskInfo.DiskInfo.VolumeCount
|
||||
disk.DiskInfo.DiskInfo.VolumeCount += volumeCountChange
|
||||
|
||||
glog.V(2).Infof("Applied actual storage change on disk %s: volume_count %d -> %d (change: %+d)",
|
||||
diskKey, oldCount, disk.DiskInfo.DiskInfo.VolumeCount, volumeCountChange)
|
||||
}
|
||||
}
|
||||
|
||||
// AddPendingTask is the unified function that handles both simple and complex task creation
|
||||
func (at *ActiveTopology) AddPendingTask(spec TaskSpec) error {
|
||||
// Validation
|
||||
if len(spec.Sources) == 0 {
|
||||
return fmt.Errorf("at least one source is required")
|
||||
}
|
||||
if len(spec.Destinations) == 0 {
|
||||
return fmt.Errorf("at least one destination is required")
|
||||
}
|
||||
|
||||
at.mutex.Lock()
|
||||
defer at.mutex.Unlock()
|
||||
|
||||
// Build sources array
|
||||
sources := make([]TaskSource, len(spec.Sources))
|
||||
for i, sourceSpec := range spec.Sources {
|
||||
var storageImpact StorageSlotChange
|
||||
var estimatedSize int64
|
||||
|
||||
if sourceSpec.StorageImpact != nil {
|
||||
// Use manually specified impact
|
||||
storageImpact = *sourceSpec.StorageImpact
|
||||
} else {
|
||||
// Auto-calculate based on task type and cleanup type
|
||||
storageImpact = at.calculateSourceStorageImpact(spec.TaskType, sourceSpec.CleanupType, spec.VolumeSize)
|
||||
}
|
||||
|
||||
if sourceSpec.EstimatedSize != nil {
|
||||
estimatedSize = *sourceSpec.EstimatedSize
|
||||
} else {
|
||||
estimatedSize = spec.VolumeSize // Default to volume size
|
||||
}
|
||||
|
||||
sources[i] = TaskSource{
|
||||
SourceServer: sourceSpec.ServerID,
|
||||
SourceDisk: sourceSpec.DiskID,
|
||||
StorageChange: storageImpact,
|
||||
EstimatedSize: estimatedSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Build destinations array
|
||||
destinations := make([]TaskDestination, len(spec.Destinations))
|
||||
for i, destSpec := range spec.Destinations {
|
||||
var storageImpact StorageSlotChange
|
||||
var estimatedSize int64
|
||||
|
||||
if destSpec.StorageImpact != nil {
|
||||
// Use manually specified impact
|
||||
storageImpact = *destSpec.StorageImpact
|
||||
} else {
|
||||
// Auto-calculate based on task type
|
||||
_, storageImpact = CalculateTaskStorageImpact(spec.TaskType, spec.VolumeSize)
|
||||
}
|
||||
|
||||
if destSpec.EstimatedSize != nil {
|
||||
estimatedSize = *destSpec.EstimatedSize
|
||||
} else {
|
||||
estimatedSize = spec.VolumeSize // Default to volume size
|
||||
}
|
||||
|
||||
destinations[i] = TaskDestination{
|
||||
TargetServer: destSpec.ServerID,
|
||||
TargetDisk: destSpec.DiskID,
|
||||
StorageChange: storageImpact,
|
||||
EstimatedSize: estimatedSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Create the task
|
||||
task := &taskState{
|
||||
VolumeID: spec.VolumeID,
|
||||
TaskType: spec.TaskType,
|
||||
Status: TaskStatusPending,
|
||||
StartedAt: time.Now(),
|
||||
EstimatedSize: spec.VolumeSize,
|
||||
Sources: sources,
|
||||
Destinations: destinations,
|
||||
}
|
||||
|
||||
at.pendingTasks[spec.TaskID] = task
|
||||
at.assignTaskToDisk(task)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreMaintenanceTask restores a task from persistent storage into the active topology
|
||||
func (at *ActiveTopology) RestoreMaintenanceTask(taskID string, volumeID uint32, taskType TaskType, status TaskStatus, sources []TaskSource, destinations []TaskDestination, estimatedSize int64) error {
|
||||
at.mutex.Lock()
|
||||
defer at.mutex.Unlock()
|
||||
|
||||
task := &taskState{
|
||||
VolumeID: volumeID,
|
||||
TaskType: taskType,
|
||||
Status: status,
|
||||
StartedAt: time.Now(), // Fallback if not provided, will be updated by heartbeats
|
||||
EstimatedSize: estimatedSize,
|
||||
Sources: sources,
|
||||
Destinations: destinations,
|
||||
}
|
||||
|
||||
if status == TaskStatusInProgress {
|
||||
at.assignedTasks[taskID] = task
|
||||
} else if status == TaskStatusPending {
|
||||
at.pendingTasks[taskID] = task
|
||||
} else {
|
||||
return nil // Ignore other statuses for topology tracking
|
||||
}
|
||||
|
||||
// Re-register task with disks for capacity tracking
|
||||
at.assignTaskToDisk(task)
|
||||
|
||||
glog.V(1).Infof("Restored %s task %s in topology: volume %d, %d sources, %d destinations",
|
||||
taskType, taskID, volumeID, len(sources), len(destinations))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasTask checks if there is any pending or assigned task for the given volume and task type.
|
||||
// If taskType is TaskTypeNone, it checks for ANY task type.
|
||||
func (at *ActiveTopology) HasTask(volumeID uint32, taskType TaskType) bool {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
for _, task := range at.pendingTasks {
|
||||
if task.VolumeID == volumeID && (taskType == TaskTypeNone || task.TaskType == taskType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, task := range at.assignedTasks {
|
||||
if task.VolumeID == volumeID && (taskType == TaskTypeNone || task.TaskType == taskType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HasAnyTask checks if there is any pending or assigned task for the given volume across all types.
|
||||
func (at *ActiveTopology) HasAnyTask(volumeID uint32) bool {
|
||||
return at.HasTask(volumeID, TaskTypeNone)
|
||||
}
|
||||
|
||||
// GetTaskServerAdjustments returns per-server volume count adjustments for
|
||||
// pending and assigned tasks of the given type. For each task, source servers
|
||||
// are decremented and destination servers are incremented, reflecting the
|
||||
// projected volume distribution once in-flight tasks complete.
|
||||
func (at *ActiveTopology) GetTaskServerAdjustments(taskType TaskType) map[string]int {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
adjustments := make(map[string]int)
|
||||
for _, task := range at.pendingTasks {
|
||||
if task.TaskType != taskType {
|
||||
continue
|
||||
}
|
||||
for _, src := range task.Sources {
|
||||
adjustments[src.SourceServer]--
|
||||
}
|
||||
for _, dst := range task.Destinations {
|
||||
adjustments[dst.TargetServer]++
|
||||
}
|
||||
}
|
||||
for _, task := range at.assignedTasks {
|
||||
if task.TaskType != taskType {
|
||||
continue
|
||||
}
|
||||
for _, src := range task.Sources {
|
||||
adjustments[src.SourceServer]--
|
||||
}
|
||||
for _, dst := range task.Destinations {
|
||||
adjustments[dst.TargetServer]++
|
||||
}
|
||||
}
|
||||
return adjustments
|
||||
}
|
||||
|
||||
// calculateSourceStorageImpact calculates storage impact for sources based on task type and cleanup type
|
||||
func (at *ActiveTopology) calculateSourceStorageImpact(taskType TaskType, cleanupType SourceCleanupType, volumeSize int64) StorageSlotChange {
|
||||
switch taskType {
|
||||
case TaskTypeErasureCoding:
|
||||
switch cleanupType {
|
||||
case CleanupVolumeReplica:
|
||||
impact, _ := CalculateTaskStorageImpact(TaskTypeErasureCoding, volumeSize)
|
||||
return impact
|
||||
case CleanupECShards:
|
||||
return CalculateECShardCleanupImpact(volumeSize)
|
||||
default:
|
||||
impact, _ := CalculateTaskStorageImpact(TaskTypeErasureCoding, volumeSize)
|
||||
return impact
|
||||
}
|
||||
default:
|
||||
impact, _ := CalculateTaskStorageImpact(taskType, volumeSize)
|
||||
return impact
|
||||
}
|
||||
}
|
||||
|
||||
// SourceCleanupType indicates what type of data needs to be cleaned up from a source
|
||||
type SourceCleanupType int
|
||||
|
||||
const (
|
||||
CleanupVolumeReplica SourceCleanupType = iota // Clean up volume replica (frees volume slots)
|
||||
CleanupECShards // Clean up existing EC shards (frees shard slots)
|
||||
)
|
||||
|
||||
// TaskSourceSpec represents a source specification for task creation
|
||||
type TaskSourceSpec struct {
|
||||
ServerID string
|
||||
DiskID uint32
|
||||
DataCenter string // Data center of the source server
|
||||
Rack string // Rack of the source server
|
||||
CleanupType SourceCleanupType // For EC: volume replica vs existing shards
|
||||
ShardIds []uint32 // For CleanupECShards: shard ids on the source disk to clear before re-distributing
|
||||
StorageImpact *StorageSlotChange // Optional: manual override
|
||||
EstimatedSize *int64 // Optional: manual override
|
||||
}
|
||||
|
||||
// TaskDestinationSpec represents a destination specification for task creation
|
||||
type TaskDestinationSpec struct {
|
||||
ServerID string
|
||||
DiskID uint32
|
||||
StorageImpact *StorageSlotChange // Optional: manual override
|
||||
EstimatedSize *int64 // Optional: manual override
|
||||
}
|
||||
|
||||
// TaskSpec represents a complete task specification
|
||||
type TaskSpec struct {
|
||||
TaskID string
|
||||
TaskType TaskType
|
||||
VolumeID uint32
|
||||
VolumeSize int64 // Used for auto-calculation when manual impacts not provided
|
||||
Sources []TaskSourceSpec // Can be single or multiple
|
||||
Destinations []TaskDestinationSpec // Can be single or multiple
|
||||
}
|
||||
362
weed/admin/topology/topology_management.go
Normal file
362
weed/admin/topology/topology_management.go
Normal file
@@ -0,0 +1,362 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
||||
)
|
||||
|
||||
// CountTopologyResources counts datacenters, nodes, and disks in topology info
|
||||
func CountTopologyResources(topologyInfo *master_pb.TopologyInfo) (dcCount, nodeCount, diskCount int) {
|
||||
if topologyInfo == nil {
|
||||
return 0, 0, 0
|
||||
}
|
||||
dcCount = len(topologyInfo.DataCenterInfos)
|
||||
for _, dc := range topologyInfo.DataCenterInfos {
|
||||
for _, rack := range dc.RackInfos {
|
||||
nodeCount += len(rack.DataNodeInfos)
|
||||
for _, node := range rack.DataNodeInfos {
|
||||
// DiskInfos is keyed by disk type, so same-type physical disks
|
||||
// collapse into one entry. Count physical disks so the number
|
||||
// matches the per-disk activeDisk map.
|
||||
for _, diskInfo := range node.DiskInfos {
|
||||
diskCount += len(diskInfo.SplitByPhysicalDisk())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateTopology updates the topology information from master
|
||||
func (at *ActiveTopology) UpdateTopology(topologyInfo *master_pb.TopologyInfo) error {
|
||||
at.mutex.Lock()
|
||||
defer at.mutex.Unlock()
|
||||
|
||||
// Validate topology updates to prevent clearing disk maps with invalid data
|
||||
if topologyInfo == nil {
|
||||
glog.Warningf("UpdateTopology received nil topologyInfo, preserving last-known-good topology")
|
||||
return fmt.Errorf("rejected invalid topology update: nil topologyInfo")
|
||||
}
|
||||
|
||||
if len(topologyInfo.DataCenterInfos) == 0 {
|
||||
glog.Warningf("UpdateTopology received empty DataCenterInfos, preserving last-known-good topology (had %d nodes, %d disks)",
|
||||
len(at.nodes), len(at.disks))
|
||||
return fmt.Errorf("rejected invalid topology update: empty DataCenterInfos (had %d nodes, %d disks)", len(at.nodes), len(at.disks))
|
||||
}
|
||||
|
||||
// Count incoming topology for validation logging
|
||||
dcCount, incomingNodes, incomingDisks := CountTopologyResources(topologyInfo)
|
||||
|
||||
// Reject updates that would wipe out a valid topology with an empty one (e.g. during master restart)
|
||||
if incomingNodes == 0 && len(at.nodes) > 0 {
|
||||
glog.Warningf("UpdateTopology received topology with 0 nodes, preserving last-known-good topology (had %d nodes, %d disks)",
|
||||
len(at.nodes), len(at.disks))
|
||||
return fmt.Errorf("rejected invalid topology update: 0 nodes (had %d nodes, %d disks)", len(at.nodes), len(at.disks))
|
||||
}
|
||||
|
||||
glog.V(2).Infof("UpdateTopology: validating update with %d datacenters, %d nodes, %d disks (current: %d nodes, %d disks)",
|
||||
dcCount, incomingNodes, incomingDisks, len(at.nodes), len(at.disks))
|
||||
|
||||
at.topologyInfo = topologyInfo
|
||||
at.lastUpdated = time.Now()
|
||||
|
||||
// Rebuild structured topology
|
||||
at.nodes = make(map[string]*activeNode)
|
||||
at.disks = make(map[string]*activeDisk)
|
||||
|
||||
for _, dc := range topologyInfo.DataCenterInfos {
|
||||
for _, rack := range dc.RackInfos {
|
||||
for _, nodeInfo := range rack.DataNodeInfos {
|
||||
node := &activeNode{
|
||||
nodeID: nodeInfo.Id,
|
||||
dataCenter: dc.Id,
|
||||
rack: rack.Id,
|
||||
nodeInfo: nodeInfo,
|
||||
disks: make(map[uint32]*activeDisk),
|
||||
}
|
||||
|
||||
// One activeDisk per physical disk_id: the master keys
|
||||
// DiskInfos by disk type, so same-type disks must be split out.
|
||||
for diskType, diskInfo := range nodeInfo.DiskInfos {
|
||||
perDiskInfos := diskInfo.SplitByPhysicalDisk()
|
||||
for _, perDisk := range perDiskInfos {
|
||||
disk := &activeDisk{
|
||||
DiskInfo: &DiskInfo{
|
||||
NodeID: nodeInfo.Id,
|
||||
Address: nodeInfo.Address,
|
||||
DiskID: perDisk.DiskId,
|
||||
DiskType: diskType,
|
||||
DataCenter: dc.Id,
|
||||
Rack: rack.Id,
|
||||
DiskInfo: perDisk,
|
||||
},
|
||||
}
|
||||
|
||||
diskKey := fmt.Sprintf("%s:%d", nodeInfo.Id, perDisk.DiskId)
|
||||
glog.V(3).Infof("UpdateTopology: adding disk key=%q nodeId=%q diskId=%d diskType=%q address=%q grpcPort=%d volumes=%d maxVolumes=%d",
|
||||
diskKey, nodeInfo.Id, perDisk.DiskId, diskType, nodeInfo.Address, nodeInfo.GrpcPort, perDisk.VolumeCount, perDisk.MaxVolumeCount)
|
||||
node.disks[perDisk.DiskId] = disk
|
||||
at.disks[diskKey] = disk
|
||||
}
|
||||
}
|
||||
|
||||
at.nodes[nodeInfo.Id] = node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild performance indexes for O(1) lookups
|
||||
at.rebuildIndexes()
|
||||
|
||||
// Reassign task states to updated topology
|
||||
at.reassignTaskStates()
|
||||
|
||||
glog.V(1).Infof("ActiveTopology updated: %d nodes, %d disks, %d volume entries, %d EC shard entries",
|
||||
len(at.nodes), len(at.disks), len(at.volumeIndex), len(at.ecShardIndex))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAvailableDisks returns disks that can accept new tasks of the given type
|
||||
// NOTE: For capacity-aware operations, prefer GetDisksWithEffectiveCapacity
|
||||
func (at *ActiveTopology) GetAvailableDisks(taskType TaskType, excludeNodeID string) []*DiskInfo {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
var available []*DiskInfo
|
||||
|
||||
for _, disk := range at.disks {
|
||||
if disk.NodeID == excludeNodeID {
|
||||
continue // Skip excluded node
|
||||
}
|
||||
|
||||
if at.isDiskAvailable(disk, taskType) {
|
||||
// Create a copy with current load count and effective capacity
|
||||
diskCopy := *disk.DiskInfo
|
||||
diskCopy.LoadCount = len(disk.pendingTasks) + len(disk.assignedTasks)
|
||||
available = append(available, &diskCopy)
|
||||
}
|
||||
}
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
// HasRecentTaskForVolume checks if a volume had a recent task (to avoid immediate re-detection)
|
||||
func (at *ActiveTopology) HasRecentTaskForVolume(volumeID uint32, taskType TaskType) bool {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
for _, task := range at.recentTasks {
|
||||
if task.VolumeID == volumeID && task.TaskType == taskType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetAllNodes returns information about all nodes (public interface)
|
||||
func (at *ActiveTopology) GetAllNodes() map[string]*master_pb.DataNodeInfo {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
result := make(map[string]*master_pb.DataNodeInfo)
|
||||
for nodeID, node := range at.nodes {
|
||||
result[nodeID] = node.nodeInfo
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetTopologyInfo returns the current topology information (read-only access)
|
||||
func (at *ActiveTopology) GetTopologyInfo() *master_pb.TopologyInfo {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
return at.topologyInfo
|
||||
}
|
||||
|
||||
// GetNodeDisks returns all disks for a specific node
|
||||
func (at *ActiveTopology) GetNodeDisks(nodeID string) []*DiskInfo {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
node, exists := at.nodes[nodeID]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
var disks []*DiskInfo
|
||||
for _, disk := range node.disks {
|
||||
diskCopy := *disk.DiskInfo
|
||||
diskCopy.LoadCount = len(disk.pendingTasks) + len(disk.assignedTasks)
|
||||
disks = append(disks, &diskCopy)
|
||||
}
|
||||
|
||||
return disks
|
||||
}
|
||||
|
||||
// GetDiskCount returns the total number of disks in the active topology
|
||||
func (at *ActiveTopology) GetDiskCount() int {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
return len(at.disks)
|
||||
}
|
||||
|
||||
// rebuildIndexes rebuilds the volume and EC shard indexes for O(1) lookups
|
||||
func (at *ActiveTopology) rebuildIndexes() {
|
||||
// Nil-safety guard: return early if topology is not valid
|
||||
if at.topologyInfo == nil || at.topologyInfo.DataCenterInfos == nil {
|
||||
glog.V(1).Infof("rebuildIndexes: skipping rebuild due to nil topology or DataCenterInfos")
|
||||
return
|
||||
}
|
||||
|
||||
// Clear existing indexes
|
||||
at.volumeIndex = make(map[uint32][]string)
|
||||
at.ecShardIndex = make(map[uint32][]string)
|
||||
|
||||
// Derive index keys from the same SplitByPhysicalDisk reconstruction that
|
||||
// builds at.disks, so every volume/EC lookup resolves to a real activeDisk
|
||||
// entry. Re-deriving the per-record disk id here would diverge on physical
|
||||
// disk 0, which the raw DiskId cannot tell apart from the "unset" default.
|
||||
for _, dc := range at.topologyInfo.DataCenterInfos {
|
||||
for _, rack := range dc.RackInfos {
|
||||
for _, nodeInfo := range rack.DataNodeInfos {
|
||||
for _, diskInfo := range nodeInfo.DiskInfos {
|
||||
for _, perDisk := range diskInfo.SplitByPhysicalDisk() {
|
||||
diskKey := fmt.Sprintf("%s:%d", nodeInfo.Id, perDisk.DiskId)
|
||||
for _, volumeInfo := range perDisk.VolumeInfos {
|
||||
at.volumeIndex[volumeInfo.Id] = append(at.volumeIndex[volumeInfo.Id], diskKey)
|
||||
}
|
||||
for _, ecShardInfo := range perDisk.EcShardInfos {
|
||||
at.ecShardIndex[ecShardInfo.Id] = append(at.ecShardIndex[ecShardInfo.Id], diskKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetVolumeLocations returns the disk locations for a volume using O(1) lookup
|
||||
func (at *ActiveTopology) GetVolumeLocations(volumeID uint32, collection string) []VolumeReplica {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
diskKeys, exists := at.volumeIndex[volumeID]
|
||||
if !exists {
|
||||
return []VolumeReplica{}
|
||||
}
|
||||
|
||||
var replicas []VolumeReplica
|
||||
for _, diskKey := range diskKeys {
|
||||
if disk, diskExists := at.disks[diskKey]; diskExists {
|
||||
// Verify collection matches (since index doesn't include collection)
|
||||
if at.volumeMatchesCollection(disk, volumeID, collection) {
|
||||
replicas = append(replicas, VolumeReplica{
|
||||
ServerID: disk.NodeID,
|
||||
DiskID: disk.DiskID,
|
||||
DataCenter: disk.DataCenter,
|
||||
Rack: disk.Rack,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return replicas
|
||||
}
|
||||
|
||||
// GetECShardLocations returns the disk locations for EC shards using O(1) lookup.
|
||||
// Each VolumeReplica.ShardIds lists the shard ids on that disk.
|
||||
func (at *ActiveTopology) GetECShardLocations(volumeID uint32, collection string) []VolumeReplica {
|
||||
at.mutex.RLock()
|
||||
defer at.mutex.RUnlock()
|
||||
|
||||
diskKeys, exists := at.ecShardIndex[volumeID]
|
||||
if !exists {
|
||||
return []VolumeReplica{}
|
||||
}
|
||||
|
||||
var ecShards []VolumeReplica
|
||||
for _, diskKey := range diskKeys {
|
||||
disk, diskExists := at.disks[diskKey]
|
||||
if !diskExists {
|
||||
continue
|
||||
}
|
||||
if !at.ecShardMatchesCollection(disk, volumeID, collection) {
|
||||
continue
|
||||
}
|
||||
shardIds := collectShardIdsForDisk(disk, volumeID, collection)
|
||||
if len(shardIds) == 0 {
|
||||
// ecShardMatchesCollection saw an info entry but every
|
||||
// EcIndexBits is zero — phantom shard record; emitting it
|
||||
// would feed an EC-cleanup source with no shard ids and
|
||||
// confuse the len(ShardIds) discriminator downstream.
|
||||
continue
|
||||
}
|
||||
ecShards = append(ecShards, VolumeReplica{
|
||||
ServerID: disk.NodeID,
|
||||
DiskID: disk.DiskID,
|
||||
DataCenter: disk.DataCenter,
|
||||
Rack: disk.Rack,
|
||||
ShardIds: shardIds,
|
||||
})
|
||||
}
|
||||
|
||||
return ecShards
|
||||
}
|
||||
|
||||
// collectShardIdsForDisk unions every matching EcIndexBits on the disk and
|
||||
// expands the bitmap into shard ids, so multiple info entries for the same
|
||||
// volume don't produce duplicates.
|
||||
func collectShardIdsForDisk(disk *activeDisk, volumeID uint32, collection string) []uint32 {
|
||||
if disk == nil || disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
|
||||
return nil
|
||||
}
|
||||
var bits erasure_coding.ShardBits
|
||||
for _, ecShardInfo := range disk.DiskInfo.DiskInfo.EcShardInfos {
|
||||
if ecShardInfo.Id != volumeID || ecShardInfo.Collection != collection {
|
||||
continue
|
||||
}
|
||||
bits |= erasure_coding.ShardBits(ecShardInfo.EcIndexBits)
|
||||
}
|
||||
if bits == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]uint32, 0, bits.Count())
|
||||
for id := uint32(0); id < erasure_coding.MaxShardCount; id++ {
|
||||
if uint32(bits)&(1<<id) != 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// volumeMatchesCollection checks if a volume on a disk matches the given collection
|
||||
func (at *ActiveTopology) volumeMatchesCollection(disk *activeDisk, volumeID uint32, collection string) bool {
|
||||
if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, volumeInfo := range disk.DiskInfo.DiskInfo.VolumeInfos {
|
||||
if volumeInfo.Id == volumeID && volumeInfo.Collection == collection {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ecShardMatchesCollection checks if EC shards on a disk match the given collection
|
||||
func (at *ActiveTopology) ecShardMatchesCollection(disk *activeDisk, volumeID uint32, collection string) bool {
|
||||
if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, ecShardInfo := range disk.DiskInfo.DiskInfo.EcShardInfos {
|
||||
if ecShardInfo.Id == volumeID && ecShardInfo.Collection == collection {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
99
weed/admin/topology/types.go
Normal file
99
weed/admin/topology/types.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package topology
|
||||
|
||||
import "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
||||
|
||||
// TaskType represents different types of maintenance operations
|
||||
type TaskType string
|
||||
|
||||
// TaskStatus represents the current status of a task
|
||||
type TaskStatus string
|
||||
|
||||
// Common task type constants
|
||||
const (
|
||||
TaskTypeNone TaskType = ""
|
||||
TaskTypeVacuum TaskType = "vacuum"
|
||||
TaskTypeBalance TaskType = "balance"
|
||||
TaskTypeErasureCoding TaskType = "erasure_coding"
|
||||
TaskTypeReplication TaskType = "replication"
|
||||
TaskTypeECBalance TaskType = "ec_balance"
|
||||
)
|
||||
|
||||
// Common task status constants
|
||||
const (
|
||||
TaskStatusPending TaskStatus = "pending"
|
||||
TaskStatusInProgress TaskStatus = "in_progress"
|
||||
TaskStatusCompleted TaskStatus = "completed"
|
||||
)
|
||||
|
||||
// Task and capacity management configuration constants
|
||||
const (
|
||||
// MaxConcurrentTasksPerDisk defines the maximum number of pending+assigned tasks per disk.
|
||||
// Set to 0 to disable hard load capping and rely on effective capacity checks.
|
||||
MaxConcurrentTasksPerDisk = 0
|
||||
|
||||
// MaxTotalTaskLoadPerDisk defines the maximum total planning load (pending + active) per disk.
|
||||
// Set to 0 to disable hard load capping for planning.
|
||||
MaxTotalTaskLoadPerDisk = 0
|
||||
|
||||
// MaxTaskLoadForECPlacement defines the maximum task load to consider a disk for EC placement.
|
||||
// Set to 0 to disable this filter.
|
||||
MaxTaskLoadForECPlacement = 0
|
||||
)
|
||||
|
||||
// StorageSlotChange represents storage impact at both volume and shard levels
|
||||
type StorageSlotChange struct {
|
||||
VolumeSlots int32 `json:"volume_slots"` // Volume-level slot changes (full volumes)
|
||||
ShardSlots int32 `json:"shard_slots"` // Shard-level slot changes (EC shards, fractional capacity)
|
||||
}
|
||||
|
||||
// Add returns a new StorageSlotChange with the sum of this and other
|
||||
func (s StorageSlotChange) Add(other StorageSlotChange) StorageSlotChange {
|
||||
return StorageSlotChange{
|
||||
VolumeSlots: s.VolumeSlots + other.VolumeSlots,
|
||||
ShardSlots: s.ShardSlots + other.ShardSlots,
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract returns a new StorageSlotChange with other subtracted from this
|
||||
func (s StorageSlotChange) Subtract(other StorageSlotChange) StorageSlotChange {
|
||||
return StorageSlotChange{
|
||||
VolumeSlots: s.VolumeSlots - other.VolumeSlots,
|
||||
ShardSlots: s.ShardSlots - other.ShardSlots,
|
||||
}
|
||||
}
|
||||
|
||||
// AddInPlace adds other to this StorageSlotChange in-place
|
||||
func (s *StorageSlotChange) AddInPlace(other StorageSlotChange) {
|
||||
s.VolumeSlots += other.VolumeSlots
|
||||
s.ShardSlots += other.ShardSlots
|
||||
}
|
||||
|
||||
// SubtractInPlace subtracts other from this StorageSlotChange in-place
|
||||
func (s *StorageSlotChange) SubtractInPlace(other StorageSlotChange) {
|
||||
s.VolumeSlots -= other.VolumeSlots
|
||||
s.ShardSlots -= other.ShardSlots
|
||||
}
|
||||
|
||||
// IsZero returns true if both VolumeSlots and ShardSlots are zero
|
||||
func (s StorageSlotChange) IsZero() bool {
|
||||
return s.VolumeSlots == 0 && s.ShardSlots == 0
|
||||
}
|
||||
|
||||
// ShardsPerVolumeSlot defines how many EC shards are equivalent to one volume slot
|
||||
const ShardsPerVolumeSlot = erasure_coding.DataShardsCount
|
||||
|
||||
// ToVolumeSlots converts the entire StorageSlotChange to equivalent volume slots
|
||||
func (s StorageSlotChange) ToVolumeSlots() int64 {
|
||||
return int64(s.VolumeSlots) + int64(s.ShardSlots)/ShardsPerVolumeSlot
|
||||
}
|
||||
|
||||
// ToShardSlots converts the entire StorageSlotChange to equivalent shard slots
|
||||
func (s StorageSlotChange) ToShardSlots() int32 {
|
||||
return s.ShardSlots + s.VolumeSlots*ShardsPerVolumeSlot
|
||||
}
|
||||
|
||||
// CanAccommodate checks if this StorageSlotChange can accommodate the required StorageSlotChange
|
||||
// Both are converted to shard slots for a more precise comparison
|
||||
func (s StorageSlotChange) CanAccommodate(required StorageSlotChange) bool {
|
||||
return s.ToShardSlots() >= required.ToShardSlots()
|
||||
}
|
||||
Reference in New Issue
Block a user