mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-17 19:48:35 +02:00
* fix(topology): capacity queries no longer create empty disk-type entries AvailableSpaceFor and CapacityFor called getOrCreateDisk, so asking a node whether it had room for a disk type it lacks created a zeroed DiskUsageCounts entry for that type. Volume growth walks every data center, rack and data node, and ToDataNodeInfo publishes diskUsages, so every node ended up advertising an empty disk (MaxVolumeCount 0, DiskId 0) of every type ever grown: HDD-only servers showed an "ssd" disk and SSD-only servers an "hdd" disk. Read the counters through a non-creating getDisk; a missing entry answers 0, the same numbers as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Et288CnR1mxDyQNdYNMBNq * fix(admin): don't let an empty disk-type entry shadow a node's real disk UpdateTopology keys disks by node and disk id. The master published an empty entry (no capacity, nothing on it, DiskId 0) for every disk type a capacity check had asked about, and it collided with the node's real disk 0; map iteration order decided which survived. An SSD-only node intermittently showed an empty HDD disk, and an HDD node lost its disk -- and with it its volumes from the volume index, hence "Could not find volume N on source server" in balance detection. IsShadowedEmptyDiskEntry matches an empty entry whose disk id another DiskInfo of the node, one with capacity or data, already uses. UpdateTopology and CountTopologyResources skip it. A lone empty entry is still an empty disk of unknown capacity and is kept, as GetDisksWithEffectiveCapacity expects. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Et288CnR1mxDyQNdYNMBNq * fix(balance): don't seed balance candidates from shadowed empty disk entries detectForDiskType registered every node with a DiskInfos entry of the type as a zero-volume candidate. An empty entry the master made up for a type the node lacks made that node the least-loaded destination, and its zero max switched the whole disk type to raw-count balancing. The volume server then refused every move: "no space left hdd" / "no space left ssd". Skip entries IsShadowedEmptyDiskEntry matches when seeding candidates. A server that holds volumes of the type but reports no max still enters through the metrics and still triggers the raw-count fallback. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Et288CnR1mxDyQNdYNMBNq * fix(admin,balance): ignore made-up disk-type entries at any disk id The made-up entry is published with DiskId 0, but it need not collide with a real disk: when location 0 is unavailable (not reported) the node's usable disk is location 1. IsShadowedEmptyDiskEntry required a collision, so such an entry survived both UpdateTopology -- where the unknown-capacity fallback offers it to planners -- and balance detection, which planned moves onto it again. Its disk id cannot tell it apart: a DiskInfo with no volumes, no EC shards and no per-disk max is published with DiskId 0 whichever disk it stands for. IsPhantomDiskTypeEntry (renamed, since nothing needs to be shadowed now) matches an empty entry on any node that has a disk of another type with capacity or data. A real disk looks the same only while it offers no volume slot, when it can take no volume either. A node whose entries are all empty keeps them as disks of unknown capacity, as before. Reported by review on #11762. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Et288CnR1mxDyQNdYNMBNq * test(balance): assert where the real disks land in the empty-entry test Check through GetNodeDisks that each server's real disk sits at the subtest's disk id before planning. The id-1 case already landed on physical disk 1 (SplitByPhysicalDisk folds all-zero record ids onto the entry's DiskId), so this only makes the setup explicit; on the first revision of the fix it shows the real disk at 1 beside the made-up entry at 0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Et288CnR1mxDyQNdYNMBNq * fix(topology,balance,ec): don't publish or trust usage-only disk-type entries ToDataNodeInfo started from the raw disk-usage counters, so a node that was asked for capacity of a disk type it lacks kept advertising an empty DiskInfo for that type even though no such disk was ever reported. Skip usage entries that have no disk child and hold all-zero counters. The same stale entries still misled planners that read DiskInfos directly: the balance plugin's move precondition treated a phantom entry as proof a target has that disk type, and the EC balancer counted it as a matching disk. Filter them with IsPhantomDiskTypeEntry. --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
508 lines
15 KiB
Go
508 lines
15 KiB
Go
package topology
|
|
|
|
import (
|
|
"fmt"
|
|
"sync/atomic"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
type DataNode struct {
|
|
NodeImpl
|
|
Ip string
|
|
Port int
|
|
GrpcPort int
|
|
PublicUrl string
|
|
LastSeen int64 // unix time in seconds
|
|
Counter int // in race condition, the previous dataNode was not dead
|
|
IsTerminating bool
|
|
|
|
// maintenanceMode mirrors the volume server's own flag, reported over the
|
|
// heartbeat. A server in maintenance is being drained: the master places
|
|
// no new volumes on it and assigns no writes to the volumes it holds. Read
|
|
// on the assign and volume-growth paths without the node lock.
|
|
maintenanceMode atomic.Bool
|
|
// lookupDigest covers the volumes reachable through this node in the volume
|
|
// layouts, for comparison against what its disks actually hold.
|
|
lookupDigest atomic.Uint64
|
|
// duplicateVolumeIds records that the node last reported one volume id more
|
|
// than once, which the master cannot represent.
|
|
duplicateVolumeIds atomic.Bool
|
|
// diskMetas holds each physical disk's tags, type, and capacity from the
|
|
// heartbeat DiskTags, including disks with no volumes or EC shards.
|
|
diskMetas map[uint32]diskMeta
|
|
}
|
|
|
|
type diskMeta struct {
|
|
tags []string
|
|
diskType types.DiskType
|
|
maxVolumeCount int64
|
|
}
|
|
|
|
func NewDataNode(id string) *DataNode {
|
|
dn := &DataNode{}
|
|
dn.id = NodeId(id)
|
|
dn.nodeType = "DataNode"
|
|
dn.diskUsages = newDiskUsages()
|
|
dn.children = make(map[NodeId]Node)
|
|
dn.capacityReservations = newCapacityReservations()
|
|
dn.NodeImpl.value = dn
|
|
return dn
|
|
}
|
|
|
|
func (dn *DataNode) String() string {
|
|
dn.RLock()
|
|
defer dn.RUnlock()
|
|
return fmt.Sprintf("Node:%s, Ip:%s, Port:%d, PublicUrl:%s", dn.NodeImpl.String(), dn.Ip, dn.Port, dn.PublicUrl)
|
|
}
|
|
|
|
// InMaintenanceMode reports whether the volume server asked to be left alone
|
|
// for writes; see Topology.SetDataNodeMaintenanceMode for what that changes.
|
|
func (dn *DataNode) InMaintenanceMode() bool {
|
|
return dn.maintenanceMode.Load()
|
|
}
|
|
|
|
// SetMaintenanceMode records the flag and reports whether it changed. Prefer
|
|
// Topology.SetDataNodeMaintenanceMode, which also updates the writable lists.
|
|
func (dn *DataNode) SetMaintenanceMode(on bool) (changed bool) {
|
|
return dn.maintenanceMode.Swap(on) != on
|
|
}
|
|
|
|
func (dn *DataNode) AddOrUpdateVolume(v storage.VolumeInfo) (isNew, isChangedRO, tierTransition bool) {
|
|
dn.Lock()
|
|
defer dn.Unlock()
|
|
return dn.doAddOrUpdateVolume(v)
|
|
}
|
|
|
|
// SetVolumeReadOnly records the read-only flag a volume server reported for
|
|
// one of its volumes, ahead of the heartbeat that will repeat it.
|
|
func (dn *DataNode) SetVolumeReadOnly(vid needle.VolumeId, readOnly bool) {
|
|
dn.Lock()
|
|
defer dn.Unlock()
|
|
for _, c := range dn.children {
|
|
if c.(*Disk).SetVolumeReadOnly(vid, readOnly) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (dn *DataNode) getOrCreateDisk(diskType string) *Disk {
|
|
c, found := dn.children[NodeId(diskType)]
|
|
if !found {
|
|
c = NewDisk(diskType)
|
|
dn.doLinkChildNode(c)
|
|
}
|
|
disk := c.(*Disk)
|
|
return disk
|
|
}
|
|
|
|
func (dn *DataNode) doAddOrUpdateVolume(v storage.VolumeInfo) (isNew, isChanged, tierTransition bool) {
|
|
disk := dn.getOrCreateDisk(v.DiskType)
|
|
return disk.AddOrUpdateVolume(v)
|
|
}
|
|
|
|
// AddProvisionalVolume records a volume the master registered on its own,
|
|
// ahead of any server report naming it. See Disk.AddProvisionalVolume.
|
|
func (dn *DataNode) AddProvisionalVolume(v storage.VolumeInfo) (isNew, isChanged, tierTransition bool) {
|
|
dn.Lock()
|
|
defer dn.Unlock()
|
|
disk := dn.getOrCreateDisk(v.DiskType)
|
|
return disk.AddProvisionalVolume(v)
|
|
}
|
|
|
|
// UpdateVolumes detects new/deleted/changed volumes on a volume server
|
|
// used in master to notify master clients of these changes.
|
|
//
|
|
// changedVolumes covers every replica the disk already held whose
|
|
// classification the new report altered in a way clients must learn about:
|
|
// the ReadOnly flag flipped, or IsRemote() flipped on tier transition. The
|
|
// latter is what lets the wdclient refresh DataInRemote after the digest
|
|
// mismatch recovery path resends a full Volumes list -- that path is the
|
|
// only way a re-tiered replica reaches the master without a separate
|
|
// ChangedVolumes heartbeat.
|
|
func (dn *DataNode) UpdateVolumes(actualVolumes []storage.VolumeInfo) (newVolumes, deletedVolumes, changedVolumes []storage.VolumeInfo) {
|
|
|
|
reported := newReportedVolumes(len(actualVolumes))
|
|
for _, v := range actualVolumes {
|
|
reported.add(v.Id, v.DiskType)
|
|
}
|
|
|
|
// A volume id mounted on two disks of one server -- a stale twin re-attached
|
|
// after a disk repair -- is reported twice, but the master keys volumes by
|
|
// id alone and keeps only the last copy. Its digest can then never equal the
|
|
// server's however often the list is resent, so record it and let the
|
|
// heartbeat fall back to the full list for this node.
|
|
dn.duplicateVolumeIds.Store(reported.duplicated)
|
|
|
|
dn.Lock()
|
|
defer dn.Unlock()
|
|
|
|
keptCount := 0
|
|
for _, c := range dn.children {
|
|
disk := c.(*Disk)
|
|
for _, v := range disk.RemoveVolumesNotIn(reported) {
|
|
glog.V(0).Infoln("Deleting volume id:", v.Id)
|
|
deletedVolumes = append(deletedVolumes, v)
|
|
|
|
deltaDiskUsage := &DiskUsageCounts{}
|
|
deltaDiskUsage.volumeCount = -1
|
|
if v.IsRemote() {
|
|
deltaDiskUsage.remoteVolumeCount = -1
|
|
}
|
|
if !v.ReadOnly {
|
|
deltaDiskUsage.activeVolumeCount = -1
|
|
}
|
|
disk.UpAdjustDiskUsageDelta(types.ToDiskType(v.DiskType), deltaDiskUsage)
|
|
}
|
|
keptCount += disk.VolumeCount()
|
|
}
|
|
// Everything still on the node is also in this heartbeat, so the remainder
|
|
// is what the node is about to gain. A steady-state heartbeat gains nothing
|
|
// and must not allocate here; a reconnecting server gains all of them.
|
|
if addedCount := reported.count() - keptCount; addedCount > 0 {
|
|
newVolumes = make([]storage.VolumeInfo, 0, addedCount)
|
|
}
|
|
for _, v := range actualVolumes {
|
|
isNew, isChanged, tierTransition := dn.doAddOrUpdateVolume(v)
|
|
if isNew {
|
|
newVolumes = append(newVolumes, v)
|
|
}
|
|
if isChanged || tierTransition {
|
|
changedVolumes = append(changedVolumes, v)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (dn *DataNode) DeltaUpdateVolumes(newVolumes, deletedVolumes []storage.VolumeInfo) {
|
|
dn.Lock()
|
|
defer dn.Unlock()
|
|
|
|
for _, v := range deletedVolumes {
|
|
disk := dn.getOrCreateDisk(v.DiskType)
|
|
|
|
_, err := disk.GetVolumesById(v.Id)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
disk.DeleteVolumeById(v.Id)
|
|
|
|
deltaDiskUsage := &DiskUsageCounts{}
|
|
deltaDiskUsage.volumeCount = -1
|
|
if v.IsRemote() {
|
|
deltaDiskUsage.remoteVolumeCount = -1
|
|
}
|
|
if !v.ReadOnly {
|
|
deltaDiskUsage.activeVolumeCount = -1
|
|
}
|
|
disk.UpAdjustDiskUsageDelta(types.ToDiskType(v.DiskType), deltaDiskUsage)
|
|
}
|
|
for _, v := range newVolumes {
|
|
dn.doAddOrUpdateVolume(v)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (dn *DataNode) AdjustMaxVolumeCounts(maxVolumeCounts map[string]uint32) {
|
|
for diskType, maxVolumeCount := range maxVolumeCounts {
|
|
if maxVolumeCount == 0 {
|
|
// the volume server may have set the max to zero
|
|
continue
|
|
}
|
|
dt := types.ToDiskType(diskType)
|
|
currentDiskUsage := dn.diskUsages.getOrCreateDisk(dt)
|
|
currentDiskUsageMaxVolumeCount := atomic.LoadInt64(¤tDiskUsage.maxVolumeCount)
|
|
if currentDiskUsageMaxVolumeCount == int64(maxVolumeCount) {
|
|
continue
|
|
}
|
|
disk := dn.getOrCreateDisk(dt.String())
|
|
disk.UpAdjustDiskUsageDelta(dt, &DiskUsageCounts{
|
|
maxVolumeCount: int64(maxVolumeCount) - currentDiskUsageMaxVolumeCount,
|
|
})
|
|
}
|
|
}
|
|
|
|
// AdjustDiskUsageBytes records the physical filesystem capacity a volume server
|
|
// reports per disk type, applied as a delta so it flows through the same
|
|
// aggregation as the volume counts. Mirrors AdjustMaxVolumeCounts; entries with a
|
|
// zero total are treated as "not reported" and skipped.
|
|
func (dn *DataNode) AdjustDiskUsageBytes(diskTotalBytes, diskFreeBytes map[string]uint64) {
|
|
for diskType, totalBytes := range diskTotalBytes {
|
|
// Unlike maxVolumeCount, a 0 here is not "unset" but "not reported": let it
|
|
// flow through so a later heartbeat that drops physical-capacity reporting
|
|
// (e.g. statfs starts failing) clears the stale bytes and the gate falls
|
|
// back to slot-only instead of trusting outdated capacity.
|
|
dt := types.ToDiskType(diskType)
|
|
currentDiskUsage := dn.diskUsages.getOrCreateDisk(dt)
|
|
currentTotal := atomic.LoadInt64(¤tDiskUsage.diskTotalBytes)
|
|
currentFree := atomic.LoadInt64(¤tDiskUsage.diskFreeBytes)
|
|
newTotal := int64(totalBytes)
|
|
newFree := int64(diskFreeBytes[diskType])
|
|
if currentTotal == newTotal && currentFree == newFree {
|
|
continue
|
|
}
|
|
disk := dn.getOrCreateDisk(dt.String())
|
|
disk.UpAdjustDiskUsageDelta(dt, &DiskUsageCounts{
|
|
diskTotalBytes: newTotal - currentTotal,
|
|
diskFreeBytes: newFree - currentFree,
|
|
})
|
|
}
|
|
}
|
|
|
|
// AppendVolumeIds appends the ids of this node's volumes to all, and repeats
|
|
// the remote-tier ones on remote, without copying the volume records to read
|
|
// them.
|
|
func (dn *DataNode) AppendVolumeIds(all, remote, readOnly, readOnlyCanDelete []uint32) ([]uint32, []uint32, []uint32, []uint32) {
|
|
dn.RLock()
|
|
defer dn.RUnlock()
|
|
for _, c := range dn.children {
|
|
all, remote, readOnly, readOnlyCanDelete = c.(*Disk).AppendVolumeIds(all, remote, readOnly, readOnlyCanDelete)
|
|
}
|
|
return all, remote, readOnly, readOnlyCanDelete
|
|
}
|
|
|
|
func (dn *DataNode) GetVolumes() (ret []storage.VolumeInfo) {
|
|
dn.RLock()
|
|
defer dn.RUnlock()
|
|
total := 0
|
|
for _, c := range dn.children {
|
|
total += c.(*Disk).VolumeCount()
|
|
}
|
|
ret = make([]storage.VolumeInfo, 0, total)
|
|
for _, c := range dn.children {
|
|
ret = c.(*Disk).AppendVolumes(ret)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// HasDuplicateVolumeIds reports whether the node's last full report named one
|
|
// volume id more than once. While it does, the node's digest is not meaningful.
|
|
func (dn *DataNode) HasDuplicateVolumeIds() bool {
|
|
return dn.duplicateVolumeIds.Load()
|
|
}
|
|
|
|
// VolumeDigest summarises every volume the master believes this node holds. A
|
|
// volume server that reports a different digest has drifted from the master and
|
|
// needs to resend its volume list.
|
|
func (dn *DataNode) VolumeDigest() uint64 {
|
|
dn.RLock()
|
|
defer dn.RUnlock()
|
|
var digest uint64
|
|
for _, c := range dn.children {
|
|
digest ^= c.(*Disk).VolumeDigest()
|
|
}
|
|
return digest
|
|
}
|
|
|
|
func (dn *DataNode) GetVolumesById(id needle.VolumeId) (vInfo storage.VolumeInfo, err error) {
|
|
dn.RLock()
|
|
defer dn.RUnlock()
|
|
found := false
|
|
for _, c := range dn.children {
|
|
disk := c.(*Disk)
|
|
vInfo, err = disk.GetVolumesById(id)
|
|
if err == nil {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
return vInfo, nil
|
|
} else {
|
|
return storage.VolumeInfo{}, fmt.Errorf("volumeInfo not found")
|
|
}
|
|
}
|
|
|
|
func (dn *DataNode) GetDataCenter() *DataCenter {
|
|
rack := dn.Parent()
|
|
if rack == nil {
|
|
return nil
|
|
}
|
|
dcNode := rack.Parent()
|
|
if dcNode == nil {
|
|
return nil
|
|
}
|
|
dcValue := dcNode.GetValue()
|
|
return dcValue.(*DataCenter)
|
|
}
|
|
|
|
func (dn *DataNode) GetDataCenterId() string {
|
|
if dc := dn.GetDataCenter(); dc != nil {
|
|
return string(dc.Id())
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (dn *DataNode) GetRack() *Rack {
|
|
return dn.Parent().(*NodeImpl).value.(*Rack)
|
|
}
|
|
|
|
func (dn *DataNode) matchesLocation(option *VolumeGrowOption) bool {
|
|
if option.DataCenter != "" && dn.GetDataCenter().Id() != NodeId(option.DataCenter) {
|
|
return false
|
|
}
|
|
if option.Rack != "" && dn.GetRack().Id() != NodeId(option.Rack) {
|
|
return false
|
|
}
|
|
if option.DataNode != "" && dn.Id() != NodeId(option.DataNode) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (dn *DataNode) GetTopology() *Topology {
|
|
p := dn.Parent()
|
|
for p.Parent() != nil {
|
|
p = p.Parent()
|
|
}
|
|
t := p.(*Topology)
|
|
return t
|
|
}
|
|
|
|
func (dn *DataNode) MatchLocation(ip string, port int) bool {
|
|
return dn.Ip == ip && dn.Port == port
|
|
}
|
|
|
|
func (dn *DataNode) Url() string {
|
|
return util.JoinHostPort(dn.Ip, dn.Port)
|
|
}
|
|
|
|
func (dn *DataNode) ServerAddress() pb.ServerAddress {
|
|
return pb.NewServerAddress(dn.Ip, dn.Port, dn.GrpcPort)
|
|
}
|
|
|
|
type DataNodeInfo struct {
|
|
Url string `json:"Url"`
|
|
PublicUrl string `json:"PublicUrl"`
|
|
Volumes int64 `json:"Volumes"`
|
|
EcShards int64 `json:"EcShards"`
|
|
Max int64 `json:"Max"`
|
|
VolumeIds string `json:"VolumeIds"`
|
|
}
|
|
|
|
func (dn *DataNode) ToInfo() (info DataNodeInfo) {
|
|
info.Url = dn.Url()
|
|
info.PublicUrl = dn.PublicUrl
|
|
|
|
// aggregated volume info
|
|
var volumeCount, ecShardCount, maxVolumeCount int64
|
|
var volumeIds string
|
|
for _, diskUsage := range dn.diskUsages.usages {
|
|
volumeCount += diskUsage.volumeCount
|
|
ecShardCount += diskUsage.ecShardCount
|
|
maxVolumeCount += diskUsage.maxVolumeCount
|
|
}
|
|
|
|
for _, disk := range dn.Children() {
|
|
d := disk.(*Disk)
|
|
volumeIds += " " + d.GetVolumeIds()
|
|
}
|
|
|
|
info.Volumes = volumeCount
|
|
info.EcShards = ecShardCount
|
|
info.Max = maxVolumeCount
|
|
info.VolumeIds = volumeIds
|
|
|
|
return
|
|
}
|
|
|
|
func (dn *DataNode) ToDataNodeInfo(filter VolumeFilter) *master_pb.DataNodeInfo {
|
|
m := &master_pb.DataNodeInfo{
|
|
Id: string(dn.Id()),
|
|
// Start from disk usage counters so empty disks are still represented
|
|
// even when there are no volumes/EC shards on this data node yet.
|
|
DiskInfos: dn.diskUsages.ToDiskInfo(),
|
|
GrpcPort: uint32(dn.GrpcPort),
|
|
Address: dn.Url(), // ip:port for connecting to the volume server
|
|
}
|
|
if m.DiskInfos == nil {
|
|
m.DiskInfos = make(map[string]*master_pb.DiskInfo)
|
|
}
|
|
for diskType, diskInfo := range m.DiskInfos {
|
|
if diskInfo == nil {
|
|
m.DiskInfos[diskType] = &master_pb.DiskInfo{Type: diskType}
|
|
continue
|
|
}
|
|
diskInfo.Type = diskType
|
|
}
|
|
|
|
reported := make(map[string]bool)
|
|
for _, c := range dn.Children() {
|
|
disk := c.(*Disk)
|
|
reported[string(disk.Id())] = true
|
|
m.DiskInfos[string(disk.Id())] = disk.ToDiskInfo(filter)
|
|
}
|
|
// A zeroed usage counter with no disk child was only ever created by a
|
|
// capacity query, not by anything the server reported: drop it so the
|
|
// node does not advertise a disk type it does not have.
|
|
for diskType := range m.DiskInfos {
|
|
if !reported[diskType] {
|
|
if u := dn.diskUsages.getDisk(types.ToDiskType(diskType)); u != nil && u.snapshot() == (DiskUsageCounts{}) {
|
|
delete(m.DiskInfos, diskType)
|
|
}
|
|
}
|
|
}
|
|
|
|
dn.RLock()
|
|
metas := make(map[uint32]diskMeta, len(dn.diskMetas))
|
|
for diskID, meta := range dn.diskMetas {
|
|
metas[diskID] = meta
|
|
}
|
|
dn.RUnlock()
|
|
for _, diskInfo := range m.DiskInfos {
|
|
if diskInfo == nil {
|
|
continue
|
|
}
|
|
if meta, found := metas[diskInfo.DiskId]; found {
|
|
diskInfo.Tags = append([]string(nil), meta.tags...)
|
|
}
|
|
// Max per physical disk of this type, empty and unavailable (max 0) ones
|
|
// included. Emit only when some disk reports capacity, so an older server
|
|
// sending all zeros leaves the map nil and falls back.
|
|
diskType := types.ToDiskType(diskInfo.Type)
|
|
maxByDisk := make(map[uint32]int64)
|
|
anyCapacity := false
|
|
for diskID, meta := range metas {
|
|
if meta.diskType != diskType {
|
|
continue
|
|
}
|
|
if meta.maxVolumeCount > 0 {
|
|
anyCapacity = true
|
|
}
|
|
maxByDisk[diskID] = meta.maxVolumeCount
|
|
}
|
|
if anyCapacity {
|
|
diskInfo.MaxVolumeCountByDisk = maxByDisk
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (dn *DataNode) UpdateDiskTags(tags []*master_pb.DiskTag) {
|
|
if len(tags) == 0 {
|
|
return
|
|
}
|
|
// DiskTags is the full list on each full heartbeat; rebuild fresh to drop
|
|
// removed disks.
|
|
metas := make(map[uint32]diskMeta, len(tags))
|
|
for _, tagInfo := range tags {
|
|
if tagInfo == nil {
|
|
continue
|
|
}
|
|
metas[tagInfo.DiskId] = diskMeta{
|
|
tags: append([]string(nil), tagInfo.Tags...),
|
|
diskType: types.ToDiskType(tagInfo.Type),
|
|
maxVolumeCount: tagInfo.MaxVolumeCount,
|
|
}
|
|
}
|
|
dn.Lock()
|
|
dn.diskMetas = metas
|
|
dn.Unlock()
|
|
}
|