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