mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-17 04:36:50 +00:00
The maintenance scanner tries to plan EC destinations for every eligible volume, so clusters that can't place EC logged a warning per volume every cycle. The min-node gate already skips clusters with fewer nodes than parity shards; demote the rest to V(2).
1091 lines
41 KiB
Go
1091 lines
41 KiB
Go
package erasure_coding
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/topology"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/worker_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding/placement"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/base"
|
|
workerutil "github.com/seaweedfs/seaweedfs/weed/worker/tasks/util"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
|
)
|
|
|
|
const (
|
|
minProposalsBeforeEarlyStop = 10
|
|
maxConsecutivePlanningFailures = 10
|
|
)
|
|
|
|
// Detection implements the detection logic for erasure coding tasks.
|
|
// It respects ctx cancellation and can stop early once maxResults is reached.
|
|
func Detection(ctx context.Context, metrics []*types.VolumeHealthMetrics, clusterInfo *types.ClusterInfo, config base.TaskConfig, maxResults int) ([]*types.TaskDetectionResult, bool, error) {
|
|
if !config.IsEnabled() {
|
|
return nil, false, nil
|
|
}
|
|
|
|
if maxResults < 0 {
|
|
maxResults = 0
|
|
}
|
|
|
|
ecConfig := config.(*Config)
|
|
var results []*types.TaskDetectionResult
|
|
hasMore := false
|
|
stoppedEarly := false
|
|
now := time.Now()
|
|
quietThreshold := time.Duration(ecConfig.QuietForSeconds) * time.Second
|
|
minSizeBytes := uint64(ecConfig.MinSizeMB) * 1024 * 1024 // Configurable minimum
|
|
|
|
debugCount := 0
|
|
skippedAlreadyEC := 0
|
|
skippedTooSmall := 0
|
|
skippedCollectionFilter := 0
|
|
skippedQuietTime := 0
|
|
skippedFullness := 0
|
|
skippedRemote := 0
|
|
skippedTooFewNodes := 0
|
|
consecutivePlanningFailures := 0
|
|
|
|
var planner *ecPlacementPlanner
|
|
|
|
allowedCollections := wildcard.CompileWildcardMatchers(ecConfig.CollectionFilter)
|
|
|
|
// Cluster node count for the min-node safety gate (mirrors the shell ec.encode
|
|
// guard that refuses to encode when nodes < parity shards, so shards cannot be
|
|
// spread for fault tolerance).
|
|
clusterNodeCount := countTopologyNodes(clusterInfo.ActiveTopology)
|
|
|
|
// Group metrics by VolumeID to handle replicas and select canonical server
|
|
volumeGroups := make(map[uint32][]*types.VolumeHealthMetrics)
|
|
for _, metric := range metrics {
|
|
if ctx != nil {
|
|
if err := ctx.Err(); err != nil {
|
|
return results, hasMore, err
|
|
}
|
|
}
|
|
volumeGroups[metric.VolumeID] = append(volumeGroups[metric.VolumeID], metric)
|
|
}
|
|
|
|
groupKeys := make([]uint32, 0, len(volumeGroups))
|
|
for volumeID := range volumeGroups {
|
|
groupKeys = append(groupKeys, volumeID)
|
|
}
|
|
sort.Slice(groupKeys, func(i, j int) bool { return groupKeys[i] < groupKeys[j] })
|
|
|
|
// Iterate over groups to check criteria and creation tasks
|
|
for idx, volumeID := range groupKeys {
|
|
if ctx != nil {
|
|
if err := ctx.Err(); err != nil {
|
|
return results, hasMore, err
|
|
}
|
|
}
|
|
if maxResults > 0 && len(results) >= maxResults {
|
|
if idx+1 < len(groupKeys) {
|
|
hasMore = true
|
|
}
|
|
stoppedEarly = true
|
|
break
|
|
}
|
|
|
|
groupMetrics := volumeGroups[volumeID]
|
|
|
|
// Find canonical metric (lowest Server ID) to ensure consistent task deduplication
|
|
metric := groupMetrics[0]
|
|
for _, m := range groupMetrics {
|
|
if m.Server < metric.Server {
|
|
metric = m
|
|
}
|
|
}
|
|
|
|
// Skip if already EC volume
|
|
if metric.IsECVolume {
|
|
skippedAlreadyEC++
|
|
continue
|
|
}
|
|
|
|
// Handle the "stuck source" state from #9448: a previous encode
|
|
// succeeded but the post-encode source-delete left a regular replica
|
|
// behind, so the master heartbeats BOTH the replica AND its EC shards.
|
|
// metric.IsECVolume above is set only for the EC-side metric path, so
|
|
// the canonical metric we picked is the regular replica with
|
|
// IsECVolume=false. Re-proposing an encode in that state collides with
|
|
// the mounted shards on the targets ("ec volume %d is mounted; refusing
|
|
// overwrite") and the detector re-queues forever.
|
|
//
|
|
// We only act when the EC shard set is COMPLETE — fewer than
|
|
// totalShards present means the existing recovery branch below
|
|
// (around the `existingECShards` block) should keep its chance to
|
|
// fold the partial shards into the new task. Counting walks
|
|
// EcIndexBits to handle a single info entry carrying multiple shards.
|
|
if clusterInfo.ActiveTopology != nil {
|
|
shardCount := countExistingEcShardsForVolume(clusterInfo.ActiveTopology, metric.VolumeID, metric.Collection)
|
|
totalShards := erasure_coding.DataShardsCount + erasure_coding.ParityShardsCount
|
|
if shardCount >= totalShards {
|
|
glog.Warningf("EC Detection: Volume %d has all %d EC shards in topology; "+
|
|
"source replica on %s is orphaned (#9448).",
|
|
metric.VolumeID, totalShards, metric.Server)
|
|
if clusterInfo.GrpcDialOption != nil {
|
|
deleted, cleanupErr := cleanupOrphanSourceReplicas(ctx, clusterInfo, metric, totalShards)
|
|
switch {
|
|
case cleanupErr != nil:
|
|
// Don't fall through to a re-encode — that would just
|
|
// collide with the mounted shards again. Surface the
|
|
// failure and wait for the next cycle; the source is
|
|
// still safe.
|
|
glog.Warningf("EC Detection: failed to auto-clean orphaned source for volume %d: %v", metric.VolumeID, cleanupErr)
|
|
case deleted > 0:
|
|
glog.Infof("EC Detection: auto-cleaned %d orphaned source replica(s) for volume %d after verifying all %d EC shards present", deleted, metric.VolumeID, totalShards)
|
|
default:
|
|
glog.V(1).Infof("EC Detection: no orphaned regular replicas found in topology for volume %d (collection %q)", metric.VolumeID, metric.Collection)
|
|
}
|
|
} else {
|
|
glog.Warningf("EC Detection: no gRPC dial option available to auto-clean orphaned source for volume %d; "+
|
|
"to clean up by hand, send a targeted VolumeDelete RPC to %s only — DO NOT use the cluster-wide `volume.delete` shell command, which would also delete the EC shards.",
|
|
metric.VolumeID, metric.Server)
|
|
}
|
|
skippedAlreadyEC++
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Check minimum size requirement
|
|
if metric.Size < minSizeBytes {
|
|
skippedTooSmall++
|
|
continue
|
|
}
|
|
|
|
// Check collection filter if specified
|
|
if len(allowedCollections) > 0 && !wildcard.MatchesAnyWildcard(allowedCollections, metric.Collection) {
|
|
skippedCollectionFilter++
|
|
continue
|
|
}
|
|
|
|
// Skip remote/tiered volumes: encoding them would lose the tiering. The
|
|
// shell ec.encode excludes remote volumes for the same reason.
|
|
if metric.HasRemoteCopy {
|
|
skippedRemote++
|
|
continue
|
|
}
|
|
|
|
// Min-node safety gate: don't encode when the cluster has fewer nodes than
|
|
// this collection's parity shards — shards could not be spread to tolerate
|
|
// failures. Mirrors the shell ec.encode guard (node count < parity shards).
|
|
if clusterNodeCount > 0 && clusterNodeCount < erasure_coding.ParityShardsCount {
|
|
skippedTooFewNodes++
|
|
continue
|
|
}
|
|
|
|
// Check quiet duration and fullness criteria
|
|
if metric.Age >= quietThreshold && metric.FullnessRatio >= ecConfig.FullnessRatio {
|
|
if ctx != nil {
|
|
if err := ctx.Err(); err != nil {
|
|
return results, hasMore, err
|
|
}
|
|
}
|
|
glog.Infof("EC Detection: Volume %d meets all criteria, attempting to create task", metric.VolumeID)
|
|
|
|
// Generate task ID for ActiveTopology integration
|
|
taskID := fmt.Sprintf("ec_vol_%d_%d", metric.VolumeID, now.Unix())
|
|
|
|
result := &types.TaskDetectionResult{
|
|
TaskID: taskID, // Link to ActiveTopology pending task
|
|
TaskType: types.TaskTypeErasureCoding,
|
|
VolumeID: metric.VolumeID,
|
|
Server: metric.Server,
|
|
Collection: metric.Collection,
|
|
Priority: types.TaskPriorityLow, // EC is not urgent
|
|
Reason: fmt.Sprintf("Volume meets EC criteria: quiet for %.1fs (>%ds), fullness=%.1f%% (>%.1f%%), size=%.1fMB (>%dMB)",
|
|
metric.Age.Seconds(), ecConfig.QuietForSeconds, metric.FullnessRatio*100, ecConfig.FullnessRatio*100,
|
|
float64(metric.Size)/(1024*1024), ecConfig.MinSizeMB),
|
|
ScheduleAt: now,
|
|
}
|
|
|
|
// Plan EC destinations if ActiveTopology is available
|
|
if clusterInfo.ActiveTopology != nil {
|
|
// Check if ANY task already exists in ActiveTopology for this volume
|
|
if clusterInfo.ActiveTopology.HasAnyTask(metric.VolumeID) {
|
|
glog.V(2).Infof("EC Detection: Skipping volume %d, task already exists in ActiveTopology", metric.VolumeID)
|
|
continue
|
|
}
|
|
|
|
glog.Infof("EC Detection: ActiveTopology available, planning destinations for volume %d", metric.VolumeID)
|
|
if planner == nil {
|
|
planner = newECPlacementPlanner(clusterInfo.ActiveTopology, ecConfig.PreferredTags)
|
|
}
|
|
dataShards := erasure_coding.DataShardsCount
|
|
parityShards := erasure_coding.ParityShardsCount
|
|
multiPlan, err := planECDestinations(planner, metric, ecConfig, dataShards, parityShards)
|
|
if err != nil {
|
|
glog.V(2).Infof("Failed to plan EC destinations for volume %d: %v", metric.VolumeID, err)
|
|
consecutivePlanningFailures++
|
|
if len(results) >= minProposalsBeforeEarlyStop && consecutivePlanningFailures >= maxConsecutivePlanningFailures {
|
|
glog.Warningf("EC Detection: stopping early after %d consecutive placement failures with %d proposals already planned", consecutivePlanningFailures, len(results))
|
|
hasMore = true
|
|
stoppedEarly = true
|
|
break
|
|
}
|
|
continue // Skip this volume if destination planning fails
|
|
}
|
|
consecutivePlanningFailures = 0
|
|
glog.Infof("EC Detection: Successfully planned %d destinations for volume %d", len(multiPlan.Plans), metric.VolumeID)
|
|
|
|
// Calculate expected shard size for EC operation
|
|
// Each data shard will be approximately volumeSize / dataShards
|
|
expectedShardSize := uint64(metric.Size) / uint64(dataShards)
|
|
|
|
// Add pending EC shard task to ActiveTopology for capacity management
|
|
|
|
// Extract shard destinations from multiPlan
|
|
var shardDestinations []string
|
|
var shardDiskIDs []uint32
|
|
for _, plan := range multiPlan.Plans {
|
|
shardDestinations = append(shardDestinations, plan.TargetNode)
|
|
shardDiskIDs = append(shardDiskIDs, plan.TargetDisk)
|
|
}
|
|
|
|
// Find all volume replica locations (server + disk) from topology
|
|
glog.Infof("EC Detection: Looking for replica locations for volume %d", metric.VolumeID)
|
|
replicaLocations := findVolumeReplicaLocations(clusterInfo.ActiveTopology, metric.VolumeID, metric.Collection)
|
|
if len(replicaLocations) == 0 {
|
|
glog.Warningf("No replica locations found for volume %d, skipping EC", metric.VolumeID)
|
|
continue
|
|
}
|
|
glog.Infof("EC Detection: Found %d replica locations for volume %d", len(replicaLocations), metric.VolumeID)
|
|
|
|
// Find existing EC shards from previous failed attempts
|
|
existingECShards := findExistingECShards(clusterInfo.ActiveTopology, metric.VolumeID, metric.Collection)
|
|
|
|
// Combine volume replicas and existing EC shards for cleanup
|
|
var sources []topology.TaskSourceSpec
|
|
|
|
// Add volume replicas (will free volume slots)
|
|
for _, replica := range replicaLocations {
|
|
sources = append(sources, topology.TaskSourceSpec{
|
|
ServerID: replica.ServerID,
|
|
DiskID: replica.DiskID,
|
|
DataCenter: replica.DataCenter,
|
|
Rack: replica.Rack,
|
|
CleanupType: topology.CleanupVolumeReplica,
|
|
})
|
|
}
|
|
|
|
// Add existing EC shards (will free shard slots)
|
|
duplicateCheck := make(map[string]bool)
|
|
for _, replica := range replicaLocations {
|
|
key := fmt.Sprintf("%s:%d", replica.ServerID, replica.DiskID)
|
|
duplicateCheck[key] = true
|
|
}
|
|
|
|
for _, shard := range existingECShards {
|
|
key := fmt.Sprintf("%s:%d", shard.ServerID, shard.DiskID)
|
|
if !duplicateCheck[key] { // Avoid duplicates if EC shards are on same disk as volume replicas
|
|
sources = append(sources, topology.TaskSourceSpec{
|
|
ServerID: shard.ServerID,
|
|
DiskID: shard.DiskID,
|
|
DataCenter: shard.DataCenter,
|
|
Rack: shard.Rack,
|
|
CleanupType: topology.CleanupECShards,
|
|
ShardIds: append([]uint32(nil), shard.ShardIds...),
|
|
})
|
|
duplicateCheck[key] = true
|
|
}
|
|
}
|
|
|
|
glog.V(2).Infof("Found %d volume replicas and %d existing EC shards for volume %d (total %d cleanup sources)",
|
|
len(replicaLocations), len(existingECShards), metric.VolumeID, len(sources))
|
|
|
|
// Convert shard destinations to TaskDestinationSpec. With fewer
|
|
// disks than shards a destination holds several shards, so reserve
|
|
// capacity for the actual per-disk shard count (round-robin matches
|
|
// createECTargets) rather than assuming one shard each.
|
|
destinations := make([]topology.TaskDestinationSpec, len(shardDestinations))
|
|
shardsPerDest := distributeECShards(dataShards+parityShards, len(shardDestinations))
|
|
for i, dest := range shardDestinations {
|
|
shardCount := len(shardsPerDest[i])
|
|
shardImpact := topology.CalculateECShardStorageImpact(int32(shardCount), int64(expectedShardSize))
|
|
destSize := int64(expectedShardSize) * int64(shardCount)
|
|
destinations[i] = topology.TaskDestinationSpec{
|
|
ServerID: dest,
|
|
DiskID: shardDiskIDs[i],
|
|
StorageImpact: &shardImpact,
|
|
EstimatedSize: &destSize,
|
|
}
|
|
}
|
|
|
|
// Convert sources before mutating topology
|
|
sourcesProto, err := convertTaskSourcesToProtobuf(sources, metric.VolumeID, clusterInfo.ActiveTopology)
|
|
if err != nil {
|
|
glog.Warningf("Failed to convert sources for EC task on volume %d: %v, skipping", metric.VolumeID, err)
|
|
continue
|
|
}
|
|
|
|
err = clusterInfo.ActiveTopology.AddPendingTask(topology.TaskSpec{
|
|
TaskID: taskID,
|
|
TaskType: topology.TaskTypeErasureCoding,
|
|
VolumeID: metric.VolumeID,
|
|
VolumeSize: int64(metric.Size),
|
|
Sources: sources,
|
|
Destinations: destinations,
|
|
})
|
|
if err != nil {
|
|
glog.Warningf("Failed to add pending EC shard task to ActiveTopology for volume %d: %v", metric.VolumeID, err)
|
|
continue // Skip this volume if topology task addition fails
|
|
}
|
|
|
|
if planner != nil {
|
|
planner.applyTaskReservations(int64(metric.Size), sources, destinations)
|
|
}
|
|
|
|
glog.V(2).Infof("Added pending EC shard task %s to ActiveTopology for volume %d with %d cleanup sources and %d shard destinations",
|
|
taskID, metric.VolumeID, len(sources), len(multiPlan.Plans))
|
|
|
|
// Create unified sources and targets for EC task
|
|
result.TypedParams = &worker_pb.TaskParams{
|
|
TaskId: taskID, // Link to ActiveTopology pending task
|
|
VolumeId: metric.VolumeID,
|
|
Collection: metric.Collection,
|
|
VolumeSize: metric.Size, // Store original volume size for tracking changes
|
|
|
|
// Unified sources - all sources that will be processed/cleaned up
|
|
Sources: sourcesProto,
|
|
|
|
// Unified targets - all EC shard destinations
|
|
Targets: createECTargets(multiPlan, dataShards, parityShards),
|
|
|
|
TaskParams: &worker_pb.TaskParams_ErasureCodingParams{
|
|
ErasureCodingParams: createECTaskParams(dataShards, parityShards, metric.DiskType),
|
|
},
|
|
}
|
|
|
|
glog.V(1).Infof("Planned EC destinations for volume %d: %d shards across %d racks, %d DCs",
|
|
metric.VolumeID, len(multiPlan.Plans), multiPlan.SuccessfulRack, multiPlan.SuccessfulDCs)
|
|
} else {
|
|
glog.Warningf("No ActiveTopology available for destination planning in EC detection")
|
|
continue // Skip this volume if no topology available
|
|
}
|
|
|
|
glog.Infof("EC Detection: Successfully created EC task for volume %d, adding to results", metric.VolumeID)
|
|
results = append(results, result)
|
|
} else {
|
|
// Count debug reasons
|
|
if metric.Age < quietThreshold {
|
|
skippedQuietTime++
|
|
}
|
|
if metric.FullnessRatio < ecConfig.FullnessRatio {
|
|
skippedFullness++
|
|
}
|
|
|
|
if debugCount < 5 { // Limit to avoid spam
|
|
// Logic moved outside
|
|
}
|
|
debugCount++
|
|
}
|
|
}
|
|
|
|
// Log debug summary if no tasks were created
|
|
if len(results) == 0 && len(metrics) > 0 && !stoppedEarly {
|
|
totalVolumes := len(metrics)
|
|
glog.V(1).Infof("EC detection: No tasks created for %d volumes (skipped: %d already EC, %d too small, %d filtered, %d not quiet, %d not full, %d remote, %d too few nodes)",
|
|
totalVolumes, skippedAlreadyEC, skippedTooSmall, skippedCollectionFilter, skippedQuietTime, skippedFullness, skippedRemote, skippedTooFewNodes)
|
|
|
|
// Show details for first few volumes
|
|
for i, metric := range metrics {
|
|
if i >= 3 || metric.IsECVolume { // Limit to first 3 non-EC volumes
|
|
continue
|
|
}
|
|
sizeMB := float64(metric.Size) / (1024 * 1024)
|
|
glog.V(1).Infof("ERASURE CODING: Volume %d: size=%.1fMB (need ≥%dMB), age=%s (need ≥%s), fullness=%.1f%% (need ≥%.1f%%)",
|
|
metric.VolumeID, sizeMB, ecConfig.MinSizeMB, metric.Age.Truncate(time.Minute), quietThreshold.Truncate(time.Minute),
|
|
metric.FullnessRatio*100, ecConfig.FullnessRatio*100)
|
|
}
|
|
}
|
|
|
|
return results, hasMore, nil
|
|
}
|
|
|
|
type ecDiskState struct {
|
|
baseAvailable int64
|
|
reservedVolumes int32
|
|
reservedShardSlots int32
|
|
}
|
|
|
|
type ecPlacementPlanner struct {
|
|
activeTopology *topology.ActiveTopology
|
|
candidates []*placement.DiskCandidate
|
|
candidateByKey map[string]*placement.DiskCandidate
|
|
diskStates map[string]*ecDiskState
|
|
diskTags map[string][]string
|
|
preferredTags []string
|
|
}
|
|
|
|
func newECPlacementPlanner(activeTopology *topology.ActiveTopology, preferredTags []string) *ecPlacementPlanner {
|
|
if activeTopology == nil {
|
|
return nil
|
|
}
|
|
|
|
disks := activeTopology.GetDisksWithEffectiveCapacity(topology.TaskTypeErasureCoding, "", 0)
|
|
candidates := diskInfosToCandidates(disks)
|
|
tagsByKey := collectDiskTags(disks)
|
|
normalizedPreferredTags := util.NormalizeTagList(preferredTags)
|
|
if len(candidates) == 0 {
|
|
return &ecPlacementPlanner{
|
|
activeTopology: activeTopology,
|
|
candidates: candidates,
|
|
candidateByKey: map[string]*placement.DiskCandidate{},
|
|
diskStates: map[string]*ecDiskState{},
|
|
diskTags: tagsByKey,
|
|
preferredTags: normalizedPreferredTags,
|
|
}
|
|
}
|
|
|
|
candidateByKey := make(map[string]*placement.DiskCandidate, len(candidates))
|
|
diskStates := make(map[string]*ecDiskState, len(candidates))
|
|
for _, candidate := range candidates {
|
|
key := ecDiskKey(candidate.NodeID, candidate.DiskID)
|
|
candidateByKey[key] = candidate
|
|
diskStates[key] = &ecDiskState{
|
|
baseAvailable: int64(candidate.FreeSlots),
|
|
}
|
|
}
|
|
|
|
return &ecPlacementPlanner{
|
|
activeTopology: activeTopology,
|
|
candidates: candidates,
|
|
candidateByKey: candidateByKey,
|
|
diskStates: diskStates,
|
|
diskTags: tagsByKey,
|
|
preferredTags: normalizedPreferredTags,
|
|
}
|
|
}
|
|
|
|
func (p *ecPlacementPlanner) selectDestinations(sourceRack, sourceDC, sourceDiskType string, shardsNeeded int) ([]*placement.DiskCandidate, error) {
|
|
if p == nil || p.activeTopology == nil {
|
|
return nil, fmt.Errorf("ec placement planner is not initialized")
|
|
}
|
|
if shardsNeeded <= 0 {
|
|
return nil, fmt.Errorf("invalid shardsNeeded %d", shardsNeeded)
|
|
}
|
|
|
|
config := placement.PlacementRequest{
|
|
ShardsNeeded: shardsNeeded,
|
|
MaxShardsPerServer: 0,
|
|
MaxShardsPerRack: 0,
|
|
MaxTaskLoad: topology.MaxTaskLoadForECPlacement,
|
|
PreferDifferentServers: true,
|
|
PreferDifferentRacks: true,
|
|
// Bias placement toward disks matching the source volume's disk
|
|
// type; placement spills to other types only if the preferred
|
|
// pool can't satisfy ShardsNeeded (#9423).
|
|
PreferredDiskType: sourceDiskType,
|
|
}
|
|
|
|
var lastErr error
|
|
for _, candidates := range p.buildCandidateSets(shardsNeeded) {
|
|
if len(candidates) == 0 {
|
|
continue
|
|
}
|
|
result, err := placement.SelectDestinations(candidates, config)
|
|
if err == nil {
|
|
if result.SpilledToOtherDiskType {
|
|
glog.Warningf("EC placement spilled to disks outside preferred disk type %q to reach %d shards (source rack=%s dc=%s)",
|
|
sourceDiskType, shardsNeeded, sourceRack, sourceDC)
|
|
}
|
|
return result.SelectedDisks, nil
|
|
}
|
|
lastErr = err
|
|
}
|
|
if lastErr == nil {
|
|
lastErr = fmt.Errorf("no EC placement candidates available")
|
|
}
|
|
return nil, lastErr
|
|
}
|
|
|
|
func (p *ecPlacementPlanner) applyTaskReservations(volumeSize int64, sources []topology.TaskSourceSpec, destinations []topology.TaskDestinationSpec) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
|
|
touched := make(map[string]bool)
|
|
|
|
for _, source := range sources {
|
|
impact := p.sourceImpact(source, volumeSize)
|
|
p.applyImpact(source.ServerID, source.DiskID, impact)
|
|
p.bumpShardCount(source.ServerID, source.DiskID, impact.ShardSlots)
|
|
key := ecDiskKey(source.ServerID, source.DiskID)
|
|
if !touched[key] {
|
|
p.bumpLoad(source.ServerID, source.DiskID)
|
|
touched[key] = true
|
|
}
|
|
}
|
|
|
|
for _, dest := range destinations {
|
|
impact := p.destinationImpact(dest, volumeSize)
|
|
p.applyImpact(dest.ServerID, dest.DiskID, impact)
|
|
p.bumpShardCount(dest.ServerID, dest.DiskID, impact.ShardSlots)
|
|
key := ecDiskKey(dest.ServerID, dest.DiskID)
|
|
if !touched[key] {
|
|
p.bumpLoad(dest.ServerID, dest.DiskID)
|
|
touched[key] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *ecPlacementPlanner) sourceImpact(source topology.TaskSourceSpec, volumeSize int64) topology.StorageSlotChange {
|
|
if source.StorageImpact != nil {
|
|
return *source.StorageImpact
|
|
}
|
|
if source.CleanupType == topology.CleanupECShards {
|
|
return topology.CalculateECShardCleanupImpact(volumeSize)
|
|
}
|
|
impact, _ := topology.CalculateTaskStorageImpact(topology.TaskTypeErasureCoding, volumeSize)
|
|
return impact
|
|
}
|
|
|
|
func (p *ecPlacementPlanner) destinationImpact(dest topology.TaskDestinationSpec, volumeSize int64) topology.StorageSlotChange {
|
|
if dest.StorageImpact != nil {
|
|
return *dest.StorageImpact
|
|
}
|
|
_, impact := topology.CalculateTaskStorageImpact(topology.TaskTypeErasureCoding, volumeSize)
|
|
return impact
|
|
}
|
|
|
|
func (p *ecPlacementPlanner) applyImpact(nodeID string, diskID uint32, impact topology.StorageSlotChange) {
|
|
if impact.IsZero() {
|
|
return
|
|
}
|
|
key := ecDiskKey(nodeID, diskID)
|
|
state, ok := p.diskStates[key]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
state.reservedVolumes += impact.VolumeSlots
|
|
state.reservedShardSlots += impact.ShardSlots
|
|
|
|
available := state.baseAvailable - int64(state.reservedVolumes) - int64(state.reservedShardSlots)/int64(topology.ShardsPerVolumeSlot)
|
|
if available < 0 {
|
|
available = 0
|
|
}
|
|
|
|
if candidate, ok := p.candidateByKey[key]; ok {
|
|
candidate.FreeSlots = int(available)
|
|
candidate.VolumeCount = candidate.MaxVolumeCount - available
|
|
}
|
|
}
|
|
|
|
func (p *ecPlacementPlanner) bumpLoad(nodeID string, diskID uint32) {
|
|
key := ecDiskKey(nodeID, diskID)
|
|
if candidate, ok := p.candidateByKey[key]; ok {
|
|
candidate.LoadCount++
|
|
}
|
|
}
|
|
|
|
func (p *ecPlacementPlanner) bumpShardCount(nodeID string, diskID uint32, delta int32) {
|
|
if delta == 0 {
|
|
return
|
|
}
|
|
key := ecDiskKey(nodeID, diskID)
|
|
if candidate, ok := p.candidateByKey[key]; ok {
|
|
candidate.ShardCount += int(delta)
|
|
if candidate.ShardCount < 0 {
|
|
candidate.ShardCount = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
func ecDiskKey(nodeID string, diskID uint32) string {
|
|
return fmt.Sprintf("%s:%d", nodeID, diskID)
|
|
}
|
|
|
|
func collectDiskTags(disks []*topology.DiskInfo) map[string][]string {
|
|
tagMap := make(map[string][]string, len(disks))
|
|
for _, disk := range disks {
|
|
if disk == nil || disk.DiskInfo == nil {
|
|
continue
|
|
}
|
|
key := ecDiskKey(disk.NodeID, disk.DiskID)
|
|
tags := util.NormalizeTagList(disk.DiskInfo.Tags)
|
|
if len(tags) > 0 {
|
|
tagMap[key] = tags
|
|
}
|
|
}
|
|
return tagMap
|
|
}
|
|
|
|
func diskHasTag(tags []string, tag string) bool {
|
|
if tag == "" || len(tags) == 0 {
|
|
return false
|
|
}
|
|
for _, candidate := range tags {
|
|
if candidate == tag {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// buildCandidateSets builds tiered candidate sets for preferred-tag prioritized placement.
|
|
// For a planner with preferredTags, it accumulates disks matching each tag in order into
|
|
// progressively larger tiers. It emits a candidate set once a tier reaches shardsNeeded,
|
|
// then continues accumulating for subsequent tags. Finally, it falls back to the full
|
|
// p.candidates set if preferred-tag tiers are insufficient. This ensures tagged disks
|
|
// are selected first before falling back to all available candidates.
|
|
func (p *ecPlacementPlanner) buildCandidateSets(shardsNeeded int) [][]*placement.DiskCandidate {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
if len(p.preferredTags) == 0 {
|
|
return [][]*placement.DiskCandidate{p.candidates}
|
|
}
|
|
selected := make(map[string]bool, len(p.candidates))
|
|
var tier []*placement.DiskCandidate
|
|
var candidateSets [][]*placement.DiskCandidate
|
|
for _, tag := range p.preferredTags {
|
|
for _, candidate := range p.candidates {
|
|
key := ecDiskKey(candidate.NodeID, candidate.DiskID)
|
|
if selected[key] {
|
|
continue
|
|
}
|
|
if diskHasTag(p.diskTags[key], tag) {
|
|
selected[key] = true
|
|
tier = append(tier, candidate)
|
|
}
|
|
}
|
|
if shardsNeeded > 0 && len(tier) >= shardsNeeded {
|
|
candidateSets = append(candidateSets, append([]*placement.DiskCandidate(nil), tier...))
|
|
}
|
|
}
|
|
// Defensive check: selectDestinations always ensures shardsNeeded > 0 before calling
|
|
// buildCandidateSets, but this branch handles direct callers and edge cases.
|
|
if shardsNeeded <= 0 && len(tier) > 0 {
|
|
candidateSets = append(candidateSets, append([]*placement.DiskCandidate(nil), tier...))
|
|
}
|
|
if len(tier) < len(p.candidates) {
|
|
candidateSets = append(candidateSets, p.candidates)
|
|
} else if len(candidateSets) == 0 {
|
|
candidateSets = append(candidateSets, p.candidates)
|
|
}
|
|
return candidateSets
|
|
}
|
|
|
|
// planECDestinations plans the destinations for erasure coding operation.
|
|
// dataShards/parityShards are parameters so callers can drive non-10+4 ratios.
|
|
// countTopologyNodes counts volume-server nodes in the active topology, used by
|
|
// the min-node safety gate.
|
|
func countTopologyNodes(at *topology.ActiveTopology) int {
|
|
if at == nil {
|
|
return 0
|
|
}
|
|
topo := at.GetTopologyInfo()
|
|
if topo == nil {
|
|
return 0
|
|
}
|
|
n := 0
|
|
for _, dc := range topo.DataCenterInfos {
|
|
for _, rack := range dc.RackInfos {
|
|
n += len(rack.DataNodeInfos)
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func planECDestinations(planner *ecPlacementPlanner, metric *types.VolumeHealthMetrics, ecConfig *Config, dataShards, parityShards int) (*topology.MultiDestinationPlan, error) {
|
|
if planner == nil || planner.activeTopology == nil {
|
|
return nil, fmt.Errorf("active topology not available for EC placement")
|
|
}
|
|
if dataShards <= 0 || parityShards <= 0 {
|
|
return nil, fmt.Errorf("invalid EC ratio: dataShards=%d parityShards=%d", dataShards, parityShards)
|
|
}
|
|
totalShards := dataShards + parityShards
|
|
// Survive losing one disk: each disk holds at most parityShards shards,
|
|
// so we need at least ceil(totalShards / parityShards) disks.
|
|
minTotalDisks := (totalShards + parityShards - 1) / parityShards
|
|
expectedShardSize := uint64(metric.Size) / uint64(dataShards)
|
|
|
|
// Get source node information from topology
|
|
var sourceRack, sourceDC string
|
|
|
|
// Extract rack and DC from topology info
|
|
topologyInfo := planner.activeTopology.GetTopologyInfo()
|
|
if topologyInfo != nil {
|
|
for _, dc := range topologyInfo.DataCenterInfos {
|
|
for _, rack := range dc.RackInfos {
|
|
for _, dataNodeInfo := range rack.DataNodeInfos {
|
|
if dataNodeInfo.Id == metric.Server {
|
|
sourceDC = dc.Id
|
|
sourceRack = rack.Id
|
|
break
|
|
}
|
|
}
|
|
if sourceRack != "" {
|
|
break
|
|
}
|
|
}
|
|
if sourceDC != "" {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Select best disks for EC placement with rack/DC diversity using the cached planner.
|
|
// Pass source disk type so placement prefers matching-type disks (#9423).
|
|
selectedDisks, err := planner.selectDestinations(sourceRack, sourceDC, metric.DiskType, totalShards)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(selectedDisks) < minTotalDisks {
|
|
return nil, fmt.Errorf("found %d disks, but EC %d+%d needs at least %d disks so no disk holds more than %d shards",
|
|
len(selectedDisks), dataShards, parityShards, minTotalDisks, parityShards)
|
|
}
|
|
// Fewer than totalShards disks is fine: createECTargets round-robins the
|
|
// shards across the available disks, packing several distinct shards onto a
|
|
// disk when needed (matching ec.encode's "spread as 4,4,3,3" fallback for
|
|
// small clusters). A disk holding several shards of one volume is safe —
|
|
// each is a separate .ecNN file and ReceiveFile keys by that extension. The
|
|
// minTotalDisks floor above keeps any single disk under parityShards shards,
|
|
// so the volume still survives losing any one disk.
|
|
if len(selectedDisks) < totalShards {
|
|
glog.V(1).Infof("EC volume %d: only %d disks for %d shards, packing up to %d shards per disk",
|
|
metric.VolumeID, len(selectedDisks), totalShards, (totalShards+len(selectedDisks)-1)/len(selectedDisks))
|
|
}
|
|
|
|
var plans []*topology.DestinationPlan
|
|
rackCount := make(map[string]int)
|
|
dcCount := make(map[string]int)
|
|
|
|
for _, disk := range selectedDisks {
|
|
// Get the target server address
|
|
targetAddress, err := workerutil.ResolveServerAddress(disk.NodeID, planner.activeTopology)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to resolve address for target server %s: %v", disk.NodeID, err)
|
|
}
|
|
|
|
plan := &topology.DestinationPlan{
|
|
TargetNode: disk.NodeID,
|
|
TargetAddress: targetAddress,
|
|
TargetDisk: disk.DiskID,
|
|
TargetRack: disk.Rack,
|
|
TargetDC: disk.DataCenter,
|
|
ExpectedSize: expectedShardSize, // Set calculated EC shard size
|
|
PlacementScore: calculateECScoreCandidate(disk, sourceRack, sourceDC),
|
|
}
|
|
plans = append(plans, plan)
|
|
|
|
// Count rack and DC diversity
|
|
rackKey := fmt.Sprintf("%s:%s", disk.DataCenter, disk.Rack)
|
|
rackCount[rackKey]++
|
|
dcCount[disk.DataCenter]++
|
|
}
|
|
|
|
// Log capacity utilization information using ActiveTopology's encapsulated logic
|
|
totalEffectiveCapacity := int64(0)
|
|
for _, plan := range plans {
|
|
key := ecDiskKey(plan.TargetNode, plan.TargetDisk)
|
|
if candidate, ok := planner.candidateByKey[key]; ok {
|
|
totalEffectiveCapacity += int64(candidate.FreeSlots)
|
|
}
|
|
}
|
|
|
|
glog.V(1).Infof("Planned EC destinations for volume %d (size=%d bytes): expected shard size=%d bytes, %d shards across %d racks, %d DCs, total effective capacity=%d slots",
|
|
metric.VolumeID, metric.Size, expectedShardSize, len(plans), len(rackCount), len(dcCount), totalEffectiveCapacity)
|
|
|
|
// Log storage impact for EC task (source only - EC has multiple targets handled individually)
|
|
sourceChange, _ := topology.CalculateTaskStorageImpact(topology.TaskTypeErasureCoding, int64(metric.Size))
|
|
glog.V(2).Infof("EC task capacity management: source_reserves_with_zero_impact={VolumeSlots:%d, ShardSlots:%d}, %d_targets_will_receive_shards, estimated_size=%d",
|
|
sourceChange.VolumeSlots, sourceChange.ShardSlots, len(plans), metric.Size)
|
|
glog.V(2).Infof("EC source reserves capacity but with zero StorageSlotChange impact")
|
|
|
|
return &topology.MultiDestinationPlan{
|
|
Plans: plans,
|
|
TotalShards: len(plans),
|
|
SuccessfulRack: len(rackCount),
|
|
SuccessfulDCs: len(dcCount),
|
|
}, nil
|
|
}
|
|
|
|
// distributeECShards assigns shard ids 0..totalShards-1 across numTargets
|
|
// targets round-robin, so each target holds either floor or ceil of
|
|
// totalShards/numTargets shards. When numTargets < totalShards this packs
|
|
// several shards onto a target; planECDestinations guarantees numTargets is at
|
|
// least ceil(totalShards/parityShards), so no target exceeds parityShards shards.
|
|
func distributeECShards(totalShards, numTargets int) [][]uint32 {
|
|
targetShards := make([][]uint32, numTargets)
|
|
for i := range targetShards {
|
|
targetShards[i] = make([]uint32, 0)
|
|
}
|
|
for shardId := 0; shardId < totalShards; shardId++ {
|
|
targetIndex := shardId % numTargets
|
|
targetShards[targetIndex] = append(targetShards[targetIndex], uint32(shardId))
|
|
}
|
|
return targetShards
|
|
}
|
|
|
|
// createECTargets builds TaskTargets, round-robining shards across the plan
|
|
// entries. With fewer disks than shards a target receives several shard ids.
|
|
func createECTargets(multiPlan *topology.MultiDestinationPlan, dataShards, parityShards int) []*worker_pb.TaskTarget {
|
|
var targets []*worker_pb.TaskTarget
|
|
numTargets := len(multiPlan.Plans)
|
|
totalShards := dataShards + parityShards
|
|
|
|
targetShards := distributeECShards(totalShards, numTargets)
|
|
|
|
for i, plan := range multiPlan.Plans {
|
|
target := &worker_pb.TaskTarget{
|
|
Node: plan.TargetAddress,
|
|
DiskId: plan.TargetDisk,
|
|
Rack: plan.TargetRack,
|
|
DataCenter: plan.TargetDC,
|
|
ShardIds: targetShards[i],
|
|
EstimatedSize: plan.ExpectedSize,
|
|
}
|
|
targets = append(targets, target)
|
|
|
|
assignedData := make([]uint32, 0)
|
|
assignedParity := make([]uint32, 0)
|
|
for _, shardId := range targetShards[i] {
|
|
if int(shardId) < dataShards {
|
|
assignedData = append(assignedData, shardId)
|
|
} else {
|
|
assignedParity = append(assignedParity, shardId)
|
|
}
|
|
}
|
|
glog.V(2).Infof("EC planning: target %s assigned shards %v (data: %v, parity: %v)",
|
|
plan.TargetNode, targetShards[i], assignedData, assignedParity)
|
|
}
|
|
|
|
glog.V(1).Infof("EC planning: distributed %d shards across %d targets using round-robin (data shards 0-%d, parity shards %d-%d)",
|
|
totalShards, numTargets, dataShards-1, dataShards, totalShards-1)
|
|
return targets
|
|
}
|
|
|
|
// convertTaskSourcesToProtobuf converts topology.TaskSourceSpec to worker_pb.TaskSource
|
|
func convertTaskSourcesToProtobuf(sources []topology.TaskSourceSpec, volumeID uint32, activeTopology *topology.ActiveTopology) ([]*worker_pb.TaskSource, error) {
|
|
var protobufSources []*worker_pb.TaskSource
|
|
|
|
for _, source := range sources {
|
|
serverAddress, err := workerutil.ResolveServerAddress(source.ServerID, activeTopology)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to resolve address for source server %s: %v", source.ServerID, err)
|
|
}
|
|
|
|
pbSource := &worker_pb.TaskSource{
|
|
Node: serverAddress,
|
|
DiskId: source.DiskID,
|
|
DataCenter: source.DataCenter,
|
|
Rack: source.Rack,
|
|
}
|
|
|
|
// Convert storage impact to estimated size
|
|
if source.EstimatedSize != nil {
|
|
pbSource.EstimatedSize = uint64(*source.EstimatedSize)
|
|
}
|
|
|
|
// Populated ShardIds is the wire-level marker that flags an
|
|
// EC-shard cleanup source; the worker routes it through
|
|
// cleanupStaleEcShards and skips it in getReplicas.
|
|
switch source.CleanupType {
|
|
case topology.CleanupVolumeReplica:
|
|
pbSource.VolumeId = volumeID
|
|
case topology.CleanupECShards:
|
|
pbSource.VolumeId = volumeID
|
|
pbSource.ShardIds = append([]uint32(nil), source.ShardIds...)
|
|
}
|
|
|
|
protobufSources = append(protobufSources, pbSource)
|
|
}
|
|
|
|
return protobufSources, nil
|
|
}
|
|
|
|
// createECTaskParams creates clean EC task parameters (destinations now in unified targets).
|
|
// sourceDiskType is forwarded to VolumeEcShardsMount so the resulting EC volume
|
|
// reports under the source's disk type rather than the target location's (#9423).
|
|
func createECTaskParams(dataShards, parityShards int, sourceDiskType string) *worker_pb.ErasureCodingTaskParams {
|
|
return &worker_pb.ErasureCodingTaskParams{
|
|
DataShards: int32(dataShards),
|
|
ParityShards: int32(parityShards),
|
|
SourceDiskType: sourceDiskType,
|
|
}
|
|
}
|
|
|
|
// diskInfosToCandidates converts topology.DiskInfo slice to placement.DiskCandidate slice
|
|
func diskInfosToCandidates(disks []*topology.DiskInfo) []*placement.DiskCandidate {
|
|
var candidates []*placement.DiskCandidate
|
|
for _, disk := range disks {
|
|
if disk.DiskInfo == nil {
|
|
continue
|
|
}
|
|
|
|
// Calculate free slots (using default max if not set)
|
|
freeSlots := int(disk.DiskInfo.MaxVolumeCount - disk.DiskInfo.VolumeCount)
|
|
if freeSlots < 0 {
|
|
freeSlots = 0
|
|
}
|
|
|
|
// Calculate EC shard count for this specific disk
|
|
// EcShardInfos contains all shards, so we need to filter by DiskId and sum actual shard counts
|
|
ecShardCount := 0
|
|
if disk.DiskInfo.EcShardInfos != nil {
|
|
for _, shardInfo := range disk.DiskInfo.EcShardInfos {
|
|
if shardInfo.DiskId == disk.DiskID {
|
|
ecShardCount += erasure_coding.GetShardCount(shardInfo)
|
|
}
|
|
}
|
|
}
|
|
|
|
candidates = append(candidates, &placement.DiskCandidate{
|
|
NodeID: disk.NodeID,
|
|
DiskID: disk.DiskID,
|
|
DataCenter: disk.DataCenter,
|
|
Rack: disk.Rack,
|
|
DiskType: disk.DiskType,
|
|
VolumeCount: disk.DiskInfo.VolumeCount,
|
|
MaxVolumeCount: disk.DiskInfo.MaxVolumeCount,
|
|
ShardCount: ecShardCount,
|
|
FreeSlots: freeSlots,
|
|
LoadCount: disk.LoadCount,
|
|
})
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
// calculateECScoreCandidate calculates placement score for EC operations.
|
|
// Used for logging and plan metadata.
|
|
func calculateECScoreCandidate(disk *placement.DiskCandidate, sourceRack, sourceDC string) float64 {
|
|
if disk == nil {
|
|
return 0.0
|
|
}
|
|
|
|
score := 0.0
|
|
|
|
// Prefer disks with available capacity (primary factor)
|
|
if disk.MaxVolumeCount > 0 {
|
|
utilization := float64(disk.VolumeCount) / float64(disk.MaxVolumeCount)
|
|
score += (1.0 - utilization) * 60.0 // Up to 60 points for available capacity
|
|
}
|
|
|
|
// Consider current load (secondary factor)
|
|
score += (10.0 - float64(disk.LoadCount)) // Up to 10 points for low load
|
|
|
|
return score
|
|
}
|
|
|
|
// findVolumeReplicaLocations finds all replica locations (server + disk) for the specified volume
|
|
// Uses O(1) indexed lookup for optimal performance on large clusters.
|
|
func findVolumeReplicaLocations(activeTopology *topology.ActiveTopology, volumeID uint32, collection string) []topology.VolumeReplica {
|
|
if activeTopology == nil {
|
|
return nil
|
|
}
|
|
return activeTopology.GetVolumeLocations(volumeID, collection)
|
|
}
|
|
|
|
// findExistingECShards finds existing EC shards for a volume (from previous failed EC attempts)
|
|
// Uses O(1) indexed lookup for optimal performance on large clusters.
|
|
func findExistingECShards(activeTopology *topology.ActiveTopology, volumeID uint32, collection string) []topology.VolumeReplica {
|
|
if activeTopology == nil {
|
|
return nil
|
|
}
|
|
return activeTopology.GetECShardLocations(volumeID, collection)
|
|
}
|
|
|
|
// cleanupOrphanSourceReplicas deletes any regular volume replicas still
|
|
// present in the topology for (volumeID, collection) after re-verifying that
|
|
// the full EC shard set is intact. Caller must hold expectedShards equal to
|
|
// the configured totalShards count. Issues VolumeDelete RPC to each replica
|
|
// server's address — that endpoint only touches the regular volume on the
|
|
// targeted server, never EC shards (those live in a separate store path).
|
|
// The cluster-wide `volume.delete` shell command is what would have nuked
|
|
// the EC shards too; the targeted RPC used here is safe by construction.
|
|
// Returns the count of replicas successfully deleted plus any error.
|
|
func cleanupOrphanSourceReplicas(ctx context.Context, clusterInfo *types.ClusterInfo, metric *types.VolumeHealthMetrics, expectedShards int) (int, error) {
|
|
if clusterInfo == nil || clusterInfo.ActiveTopology == nil {
|
|
return 0, fmt.Errorf("active topology unavailable")
|
|
}
|
|
if clusterInfo.GrpcDialOption == nil {
|
|
return 0, fmt.Errorf("grpc dial option unavailable")
|
|
}
|
|
|
|
// Re-verify shard completeness right before acting. Defensive: detection
|
|
// processes many volumes sequentially and the topology snapshot we built
|
|
// at start-of-detection could have lost shards in between (a volume
|
|
// server going down between iterations). Refusing to delete the source
|
|
// when we can no longer prove the shards are complete is the safer
|
|
// failure mode — the source replica is the only complete copy.
|
|
actualShards := countExistingEcShardsForVolume(clusterInfo.ActiveTopology, metric.VolumeID, metric.Collection)
|
|
if actualShards < expectedShards {
|
|
return 0, fmt.Errorf("EC shard set shrank between detection and cleanup (%d < %d); refusing to delete source replica", actualShards, expectedShards)
|
|
}
|
|
|
|
replicas := findVolumeReplicaLocations(clusterInfo.ActiveTopology, metric.VolumeID, metric.Collection)
|
|
if len(replicas) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
deleted := 0
|
|
var deleteErrors []string
|
|
for _, replica := range replicas {
|
|
serverAddress := replica.ServerID
|
|
err := operation.WithVolumeServerClient(false, pb.ServerAddress(serverAddress), clusterInfo.GrpcDialOption,
|
|
func(client volume_server_pb.VolumeServerClient) error {
|
|
_, deleteErr := client.VolumeDelete(ctx, &volume_server_pb.VolumeDeleteRequest{
|
|
VolumeId: metric.VolumeID,
|
|
OnlyEmpty: false,
|
|
})
|
|
return deleteErr
|
|
})
|
|
if err != nil {
|
|
deleteErrors = append(deleteErrors, fmt.Sprintf("server %s: %v", serverAddress, err))
|
|
continue
|
|
}
|
|
deleted++
|
|
glog.V(1).Infof("EC Detection: deleted orphan regular replica for volume %d on %s", metric.VolumeID, serverAddress)
|
|
}
|
|
|
|
if len(deleteErrors) > 0 {
|
|
return deleted, fmt.Errorf("%d of %d replica delete(s) failed: %s", len(deleteErrors), len(replicas), strings.Join(deleteErrors, "; "))
|
|
}
|
|
return deleted, nil
|
|
}
|
|
|
|
// countExistingEcShardsForVolume returns the number of distinct EC shard IDs
|
|
// for (volumeID, collection) present in the topology. Walks every disk's
|
|
// EcIndexBits bitmap rather than trusting len(EcShardInfos), because a single
|
|
// info entry can carry multiple shards. Used by the #9448 guard to decide
|
|
// whether the EC shard set is complete enough that the orphaned regular
|
|
// replica is safe to delete.
|
|
func countExistingEcShardsForVolume(activeTopology *topology.ActiveTopology, volumeID uint32, collection string) int {
|
|
if activeTopology == nil {
|
|
return 0
|
|
}
|
|
topologyInfo := activeTopology.GetTopologyInfo()
|
|
if topologyInfo == nil {
|
|
return 0
|
|
}
|
|
var seen erasure_coding.ShardBits
|
|
for _, dc := range topologyInfo.DataCenterInfos {
|
|
for _, rack := range dc.RackInfos {
|
|
for _, node := range rack.DataNodeInfos {
|
|
for _, diskInfo := range node.DiskInfos {
|
|
for _, ecShardInfo := range diskInfo.EcShardInfos {
|
|
if ecShardInfo.Id != volumeID || ecShardInfo.Collection != collection {
|
|
continue
|
|
}
|
|
seen |= erasure_coding.ShardBits(ecShardInfo.EcIndexBits)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return seen.Count()
|
|
}
|