mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
* Add shared super_block.ResolveReplicaPlacement; use it in ec_balance * Add ecbalancer.FromActiveTopology snapshot constructor for EC encode/repair * Add ecbalancer.Place greenfield/repair placement core (strict + durability-first) * topology: add GetEffectiveAvailableEcShardSlots; FromActiveTopology uses shard-granular free slots GetDisksWithEffectiveCapacity flattens reserved shard slots into volume slots via integer truncation, so an in-flight EC task reserving a non-multiple-of- DataShardsCount number of shards was lost from the snapshot and freeSlots was over-reported. GetEffectiveAvailableEcShardSlots subtracts the full reservation impact at shard granularity. * ecbalancer.Place: reject nodes without a free disk of the requested type FromActiveTopology keeps all disk types in the snapshot, so an SSD-only request could be routed to a node with only HDD capacity (pickBestDiskOnNode then returns disk 0 on the wrong tier). Filter rack/node selection to those with a free disk of the requested type. * ecbalancer.Place: enforce ReplicaPlacement DiffDataCenterCount (per-DC shard cap) * ecbalancer: enforce DiffDataCenterCount in balance (cross-DC phase + cross-rack DC cap) Adds a cross-DC corrective phase that drains data centers holding more than DiffDataCenterCount shards of a volume, and a per-DC cap on cross-rack move targets. Both are no-ops when DiffDataCenterCount is unset, so balance output is unchanged for non-DC placements. * topology: ratio-aware EC shard slots and provisional empty-disk slot GetEffectiveAvailableEcShardSlots now takes the target collection's data-shard count, so a 4+2 volume's larger shards are not over-counted at 10 per volume slot; and it keeps the one provisional slot for freshly started empty servers that report max=0, matching getEffectiveAvailableCapacityUnsafe. FromActiveTopology threads the ratio through. * ecbalancer.Place: explicit disk-type filter signal (fix HDD vs any ambiguity) HardDriveType normalizes to "", which collided with "" meaning any disk. Add Constraints.FilterDiskType and normalize both sides so a hdd request matches disks reported as "" and never leaks to SSD, while filter=false still means any. * ecbalancer: add clearShardAccounting for repair snapshot reconciliation Clears one disk's copy of a shard from per-domain accounting and recomputes the node-level union (preserving a kept copy on another disk of the same node), without crediting capacity. Repair uses it to drop to-be-deleted copies before placing missing shards. * ecbalancer: don't cap cross-DC target racks when DiffRackCount is unset len(racks)+1 wrongly limited each target rack (3 in a 2-rack cluster), so draining a DC could stop short of the DiffDataCenterCount cap. Use MaxShardCount+1 as the effectively-unlimited default. * topology/ecbalancer: ratio-correct EC capacity accounting Reservation shard slots (default ShardsPerVolumeSlot units) are now converted to the target ratio before subtracting, and existing EC shards are charged by size (targetDataShards/shardDataShards) so a 2+1 shard isn't counted as one 10+4 slot. Per-shard ratio lookup is behind shardDataShards (OSS uses the standard ratio). * ecbalancer.Place: candidate tiering and eligible-rack caps Adds a per-disk eligibility/preference abstraction so Place supports: - preferred-tag whole-plan retry (try disks carrying the earliest tags first, widen to all only if a tier cannot place every shard; reports SpilledOutsidePreferredTags), - soft disk-type spill via DiskTypePolicy (Any/Prefer/Require): Prefer fills the preferred type then spills, reporting SpilledToOtherDiskType; Require filters, - even per-rack caps that divide by racks holding an eligible disk, so a tiered cluster (e.g. SSDs in 2 of 4 racks) isn't capped impossibly low. Disk tags carried via Node.AddDiskTags + FromActiveTopology. * ecbalancer: export ClearShardAccounting for repair snapshot reconciliation * ecbalancer: address review feedback (ratio rounding, bitmap walk, same-DC moves) - topology/ecbalancer: round shard-reservation and existing-shard footprint up when converting to target-ratio shard slots, so a sub-slot reservation is not truncated to zero and free capacity is not overstated for low-data-shard layouts (targetDataShards < ds). - erasure_coding: add ShardBits.All iterator and use it across the balancer, cross-DC phase, and placement scoring instead of scanning 0..MaxShardCount and probing Has on every id. - ecbalancer: allow same-DC cross-rack moves when a DC already sits at its DiffDataCenterCount cap; a same-DC move leaves the DC total unchanged. Add a regression test that fails without the guard. - ecbalancer cross-DC phase: pick targets via the eligible-aware pickNodeInRackEligible/pickBestDiskEligible helpers so the disk-type filter is honored and a 0 disk id is not mistaken for a valid selection. * ecbalancer: test ecShardSlotsOnDisk fractional round-up Cover the mixed-ratio path (targetDataShards < existing data shards) so a shard's fractional footprint is never floored to zero and free capacity is not overstated. Exercises the round-up via the targetDataShards parameter; OSS uses the standard ratio at runtime while the enterprise build hits it with real per-volume ratios. * ecbalancer: assert node B rack in TestFromActiveTopology * ecbalancer: split Destination into separate DataCenter and bare Rack Replace the composite "dc:rack" Rack field on Destination with separate DataCenter and bare Rack values, matching topology.DiskInfo and the worker-task convention. Callers (and tests) read the data center directly instead of parsing the composite with strings.SplitN. * shell ec.balance: use utilization-based global balancing (parity with worker) The shell's global rebalance phase balanced by raw shard count; switch it to fractional fullness (shards/capacity), as the worker already does. On uniform capacity the two agree; on heterogeneous capacity it fills nodes proportionally instead of driving small-capacity nodes toward full. Updates the heterogeneous-capacity regression test to assert even fullness (~equal shards/capacity per node) rather than even shard count. * ecbalancer: bounded-proportional per-DC shard spread DiffDataCenterCount was enforced only as a ceiling (drain-to-cap), which could leave a within-cap-but-lopsided DC distribution under a loose cap (e.g. 10/4 of 14 with cap=10). Now the cross-DC phase, the cross-rack DC guard, and Place all target boundedMaxPerDC = min(DiffDataCenterCount, max(ceil(total/numDCs), parityShards)): shards spread proportionally across DCs, but no tighter than the durability floor (once each DC holds <= parityShards a DC loss is recoverable, so further spreading only adds cross-DC/WAN traffic). No-op when DiffDataCenterCount is 0; identical to before when the cap is the binding constraint. * ecbalancer: drop DiffDataCenterCount enforcement for EC placement The 1-byte volume ReplicaPlacement packs xyz into x*100+y*10+z<=255, so the DC digit can only be 0-2 -- far too small to be a meaningful per-DC EC shard cap (a cap of 1-2 would demand 7-14 DCs for a 10+4 volume). It's volume replica-placement, not an EC spec. Removes the cross-DC balance phase, the DC guard in the cross-rack phase, and the per-DC cap in Place (and the just-added bounded-proportional logic); EC relies on the RP-independent rack/node even spread instead. Rack/node caps (DiffRackCount/SameRackCount) are unchanged. Per-domain EC caps are left for a real EC placement spec. * ecbalancer: enforce per-disk durability cap; symmetric reserve/release Place now refuses to put more than parityShards shards of a volume on a single disk (pickBestDiskEligible skips a disk once it holds parityShards of the volume, a hard cap not relaxed even in durability-first). Previously Place assigned by free capacity, so a skewed near-full cluster could pile >parityShards onto one disk -> losing it loses the volume; only distinct-disk count was checked. This covers encode and repair (both route through Place); the caller skips/leaves the volume rather than minting an unrecoverable layout. Also makes reserveShard decrement freeSlots unconditionally, symmetric with releaseShard's unconditional increment (the old guarded decrement could credit a phantom slot on release if a shard were ever reserved onto a full disk). * ecbalancer: add Topology.ReleaseVolumeShards (clear + credit) for greenfield encode Releases all of a volume's shards from the snapshot and credits the freed disk capacity, so a greenfield encode can plan as if stale EC shards from a prior failed attempt are gone. Safe to credit because the encode task deletes stale shards (cleanupStaleEcShards) before distributing the new ones. Distinct from ClearShardAccounting (repair), which does not credit. * ecbalancer: ReleaseVolumeShards credits node freeSlots, not just disks releaseShard only increments per-disk freeSlots, but rack capacity is summed from node freeSlots (buildRacks) and node freeSlots gates node eligibility. Crediting only disks left a node/rack looking full after releasing stale shards, so a greenfield encode still couldn't use the freed capacity. Now credits the node by the total disk-slots freed. * ecbalancer: correct PlacementMode docs (encode uses durability-first) PlaceStrict was labeled '(encode)' but encode uses PlaceDurabilityFirst. Clarify that durability-first is used by both encode and repair, reports relaxations in PlaceResult.Relaxed, and never relaxes the per-disk durability cap. * ecbalancer: treat SameRackCount as a direct per-node shard cap The 3rd ReplicaPlacement digit now caps shards per node at exactly the digit value, matching how DiffRackCount (2nd digit) caps per rack, instead of allowing digit+1 per node. This makes the per-rack and per-node caps consistent and matches the documented "digits cap EC shards per rack and per node" semantics; e.g. 011 now means at most one shard per rack and one per node.
384 lines
15 KiB
Go
384 lines
15 KiB
Go
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,
|
|
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...),
|
|
}
|
|
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,
|
|
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...),
|
|
}
|
|
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)
|
|
}
|
|
}
|