mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-26 06:50:45 +01:00
* feat(master): size-aware volume assignment with weighted selection PickForWrite now selects volumes proportional to remaining capacity instead of uniform random, so emptier volumes receive more writes. - Add vid2size map to VolumeLayout tracking effective volume sizes - Weighted pick via random sampling (k=3) for O(1) cost - RecordAssign tracks estimated pending bytes between heartbeats - Exponential decay on heartbeat: halve excess each cycle - Proactive crowded detection using effective size - Zero extra heap allocations on the unconstrained hot path Benchmark (20 writable volumes, unconstrained): Before: 36 ns/op, 32 B/op, 2 allocs/op After: 85 ns/op, 32 B/op, 2 allocs/op * fix: address review feedback on size-aware assignment - RecordAssign: use write lock (Lock) instead of read lock (RLock) since it mutates vid2size map and crowded set - RegisterVolume: clear crowded flag when heartbeat decay drops effective size below the threshold - pickWeightedByRemaining: fix misleading Fisher-Yates comment, simplify to plain random sampling (duplicates are harmless) - ShouldGrowVolumesByDcAndRack: read vid2size under RLock * fix: decay once per heartbeat cycle, not per replica RegisterVolume is called once per replica of a volume. For replicated volumes, the pending size decay was running multiple times per heartbeat cycle, reducing the excess by 75% instead of 50% (for 2 replicas). Fix: track vid2reportedSize and only run decay when the heartbeat- reported size actually changes. A second replica reporting the same size in the same cycle is a no-op. Also fix CodeQL alert: cap count*EstimatedNeedleSizeBytes to avoid uint64→int64 overflow in RecordAssign call. * Potential fix for pull request finding 'CodeQL / Incorrect conversion between integer types' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix: fail fast in test setup on JSON errors - setupWithLimit now takes testing.TB and calls t.Fatalf on unmarshal errors or type assertion failures instead of printing and continuing - benchSetup removed; benchmarks reuse setupWithLimit directly * fix: run size decay on every heartbeat, not just new volumes RegisterVolume is only called for newly discovered volumes, not on every heartbeat. The pending size decay was never running in production. - Extract decay logic into UpdateVolumeSize(), called from SyncDataNodeRegistration for every reported volume on every heartbeat - RegisterVolume only initializes vid2size for brand-new volumes - Constrained PickForWrite: scan from random offset, collect up to pickSampleSize matches in a stack array (no append allocation) - Tests now exercise UpdateVolumeSize directly instead of RegisterVolume to match the production heartbeat path * fix: compute pending bytes in uint64 to satisfy CodeQL --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
688 lines
19 KiB
Go
688 lines
19 KiB
Go
package topology
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand/v2"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
)
|
|
|
|
type copyState int
|
|
|
|
const (
|
|
noCopies copyState = 0 + iota
|
|
insufficientCopies
|
|
enoughCopies
|
|
)
|
|
|
|
type volumeState string
|
|
|
|
const (
|
|
readOnlyState volumeState = "ReadOnly"
|
|
oversizedState = "Oversized"
|
|
crowdedState = "Crowded"
|
|
NoWritableVolumes = "No writable volumes"
|
|
)
|
|
|
|
type stateIndicator func(copyState) bool
|
|
|
|
func ExistCopies() stateIndicator {
|
|
return func(state copyState) bool { return state != noCopies }
|
|
}
|
|
|
|
type volumesBinaryState struct {
|
|
rp *super_block.ReplicaPlacement
|
|
name volumeState // the name for volume state (eg. "Readonly", "Oversized")
|
|
indicator stateIndicator // indicate whether the volumes should be marked as `name`
|
|
copyMap map[needle.VolumeId]*VolumeLocationList
|
|
}
|
|
|
|
func NewVolumesBinaryState(name volumeState, rp *super_block.ReplicaPlacement, indicator stateIndicator) *volumesBinaryState {
|
|
return &volumesBinaryState{
|
|
rp: rp,
|
|
name: name,
|
|
indicator: indicator,
|
|
copyMap: make(map[needle.VolumeId]*VolumeLocationList),
|
|
}
|
|
}
|
|
|
|
func (v *volumesBinaryState) Dump() (res []uint32) {
|
|
for vid, list := range v.copyMap {
|
|
if v.indicator(v.copyState(list)) {
|
|
res = append(res, uint32(vid))
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (v *volumesBinaryState) IsTrue(vid needle.VolumeId) bool {
|
|
list, _ := v.copyMap[vid]
|
|
return v.indicator(v.copyState(list))
|
|
}
|
|
|
|
func (v *volumesBinaryState) Add(vid needle.VolumeId, dn *DataNode) {
|
|
list, _ := v.copyMap[vid]
|
|
if list != nil {
|
|
list.Set(dn)
|
|
return
|
|
}
|
|
list = NewVolumeLocationList()
|
|
list.Set(dn)
|
|
v.copyMap[vid] = list
|
|
}
|
|
|
|
func (v *volumesBinaryState) Remove(vid needle.VolumeId, dn *DataNode) {
|
|
list, _ := v.copyMap[vid]
|
|
if list != nil {
|
|
list.Remove(dn)
|
|
if list.Length() == 0 {
|
|
delete(v.copyMap, vid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *volumesBinaryState) copyState(list *VolumeLocationList) copyState {
|
|
if list == nil {
|
|
return noCopies
|
|
}
|
|
if list.Length() < v.rp.GetCopyCount() {
|
|
return insufficientCopies
|
|
}
|
|
return enoughCopies
|
|
}
|
|
|
|
// mapping from volume to its locations, inverted from server to volume
|
|
type VolumeLayout struct {
|
|
growRequest atomic.Bool
|
|
lastGrowCount atomic.Uint32
|
|
rp *super_block.ReplicaPlacement
|
|
ttl *needle.TTL
|
|
diskType types.DiskType
|
|
vid2location map[needle.VolumeId]*VolumeLocationList
|
|
writables []needle.VolumeId // transient array of writable volume id
|
|
crowded map[needle.VolumeId]struct{}
|
|
readonlyVolumes *volumesBinaryState // readonly volumes
|
|
oversizedVolumes *volumesBinaryState // oversized volumes
|
|
vacuumedVolumes map[needle.VolumeId]time.Time
|
|
volumeSizeLimit uint64
|
|
replicationAsMin bool
|
|
accessLock sync.RWMutex
|
|
vid2size map[needle.VolumeId]uint64 // effective size: reported + pending
|
|
vid2reportedSize map[needle.VolumeId]uint64 // last heartbeat-reported size (dedup replicas)
|
|
}
|
|
|
|
type VolumeLayoutStats struct {
|
|
TotalSize uint64
|
|
UsedSize uint64
|
|
FileCount uint64
|
|
}
|
|
|
|
func NewVolumeLayout(rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType, volumeSizeLimit uint64, replicationAsMin bool) *VolumeLayout {
|
|
return &VolumeLayout{
|
|
rp: rp,
|
|
ttl: ttl,
|
|
diskType: diskType,
|
|
vid2location: make(map[needle.VolumeId]*VolumeLocationList),
|
|
writables: *new([]needle.VolumeId),
|
|
crowded: make(map[needle.VolumeId]struct{}),
|
|
readonlyVolumes: NewVolumesBinaryState(readOnlyState, rp, ExistCopies()),
|
|
oversizedVolumes: NewVolumesBinaryState(oversizedState, rp, ExistCopies()),
|
|
vacuumedVolumes: make(map[needle.VolumeId]time.Time),
|
|
volumeSizeLimit: volumeSizeLimit,
|
|
replicationAsMin: replicationAsMin,
|
|
vid2size: make(map[needle.VolumeId]uint64),
|
|
vid2reportedSize: make(map[needle.VolumeId]uint64),
|
|
}
|
|
}
|
|
|
|
func (vl *VolumeLayout) String() string {
|
|
return fmt.Sprintf("rp:%v, ttl:%v, writables:%v, volumeSizeLimit:%v", vl.rp, vl.ttl, vl.writables, vl.volumeSizeLimit)
|
|
}
|
|
|
|
func (vl *VolumeLayout) RegisterVolume(v *storage.VolumeInfo, dn *DataNode) {
|
|
vl.accessLock.Lock()
|
|
defer vl.accessLock.Unlock()
|
|
|
|
defer vl.rememberOversizedVolume(v, dn)
|
|
|
|
if _, ok := vl.vid2location[v.Id]; !ok {
|
|
vl.vid2location[v.Id] = NewVolumeLocationList()
|
|
}
|
|
vl.vid2location[v.Id].Set(dn)
|
|
// For new volumes, initialize vid2size from reported size.
|
|
if _, exists := vl.vid2size[v.Id]; !exists {
|
|
vl.vid2size[v.Id] = v.Size
|
|
vl.vid2reportedSize[v.Id] = v.Size
|
|
}
|
|
// glog.V(4).Infof("volume %d added to %s len %d copy %d", v.Id, dn.Id(), vl.vid2location[v.Id].Length(), v.ReplicaPlacement.GetCopyCount())
|
|
for _, dn := range vl.vid2location[v.Id].list {
|
|
if vInfo, err := dn.GetVolumesById(v.Id); err == nil {
|
|
if vInfo.ReadOnly {
|
|
glog.V(1).Infof("vid %d removed from writable", v.Id)
|
|
vl.removeFromWritable(v.Id)
|
|
vl.readonlyVolumes.Add(v.Id, dn)
|
|
return
|
|
} else {
|
|
vl.readonlyVolumes.Remove(v.Id, dn)
|
|
}
|
|
} else {
|
|
glog.V(1).Infof("vid %d removed from writable", v.Id)
|
|
vl.removeFromWritable(v.Id)
|
|
vl.readonlyVolumes.Remove(v.Id, dn)
|
|
return
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (vl *VolumeLayout) rememberOversizedVolume(v *storage.VolumeInfo, dn *DataNode) {
|
|
if vl.isOversized(v) {
|
|
vl.oversizedVolumes.Add(v.Id, dn)
|
|
} else {
|
|
vl.oversizedVolumes.Remove(v.Id, dn)
|
|
}
|
|
}
|
|
|
|
// UpdateVolumeSize is called on every heartbeat for every reported volume.
|
|
// It decays the pending size estimate toward the reported size and updates
|
|
// crowded state. Replicated volumes report from multiple DataNodes; decay
|
|
// runs only once per new reported size to avoid double-halving.
|
|
func (vl *VolumeLayout) UpdateVolumeSize(vid needle.VolumeId, reportedSize uint64) {
|
|
vl.accessLock.Lock()
|
|
defer vl.accessLock.Unlock()
|
|
|
|
if reportedSize == vl.vid2reportedSize[vid] {
|
|
return // same size from another replica in this cycle
|
|
}
|
|
vl.vid2reportedSize[vid] = reportedSize
|
|
if prev := vl.vid2size[vid]; prev > reportedSize {
|
|
vl.vid2size[vid] = reportedSize + (prev-reportedSize)/2
|
|
} else {
|
|
vl.vid2size[vid] = reportedSize
|
|
}
|
|
if float64(vl.vid2size[vid]) > float64(vl.volumeSizeLimit)*VolumeGrowStrategy.Threshold {
|
|
vl.setVolumeCrowded(vid)
|
|
} else {
|
|
vl.removeFromCrowded(vid)
|
|
}
|
|
}
|
|
|
|
func (vl *VolumeLayout) UnRegisterVolume(v *storage.VolumeInfo, dn *DataNode) {
|
|
vl.accessLock.Lock()
|
|
defer vl.accessLock.Unlock()
|
|
|
|
// remove from vid2location map
|
|
location, ok := vl.vid2location[v.Id]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if location.Remove(dn) {
|
|
|
|
vl.readonlyVolumes.Remove(v.Id, dn)
|
|
vl.oversizedVolumes.Remove(v.Id, dn)
|
|
vl.ensureCorrectWritables(v.Id)
|
|
|
|
if location.Length() == 0 {
|
|
delete(vl.vid2location, v.Id)
|
|
delete(vl.vid2size, v.Id)
|
|
delete(vl.vid2reportedSize, v.Id)
|
|
vl.removeFromCrowded(v.Id)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func (vl *VolumeLayout) EnsureCorrectWritables(v *storage.VolumeInfo) {
|
|
vl.accessLock.Lock()
|
|
defer vl.accessLock.Unlock()
|
|
|
|
vl.ensureCorrectWritables(v.Id)
|
|
}
|
|
|
|
func (vl *VolumeLayout) ensureCorrectWritables(vid needle.VolumeId) {
|
|
isEnoughCopies := vl.enoughCopies(vid)
|
|
isAllWritable := vl.isAllWritable(vid)
|
|
isOversizedVolume := vl.oversizedVolumes.IsTrue(vid)
|
|
if isEnoughCopies && isAllWritable && !isOversizedVolume {
|
|
vl.setVolumeWritable(vid)
|
|
} else {
|
|
if !isEnoughCopies {
|
|
glog.V(0).Infof("volume %d does not have enough copies", vid)
|
|
}
|
|
if !isAllWritable {
|
|
glog.V(0).Infof("volume %d are not all writable", vid)
|
|
}
|
|
if isOversizedVolume {
|
|
glog.V(1).Infof("volume %d are oversized", vid)
|
|
}
|
|
glog.V(0).Infof("volume %d remove from writable", vid)
|
|
vl.removeFromWritable(vid)
|
|
}
|
|
}
|
|
|
|
func (vl *VolumeLayout) isAllWritable(vid needle.VolumeId) bool {
|
|
if location, ok := vl.vid2location[vid]; ok {
|
|
for _, dn := range location.list {
|
|
if v, getError := dn.GetVolumesById(vid); getError == nil {
|
|
if v.ReadOnly {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (vl *VolumeLayout) isOversized(v *storage.VolumeInfo) bool {
|
|
return uint64(v.Size) >= vl.volumeSizeLimit
|
|
}
|
|
|
|
func (vl *VolumeLayout) isCrowdedVolume(v *storage.VolumeInfo) bool {
|
|
return float64(v.Size) > float64(vl.volumeSizeLimit)*VolumeGrowStrategy.Threshold
|
|
}
|
|
|
|
// RecordAssign adds the estimated byte size to the volume's tracked effective
|
|
// size and marks it crowded if it crosses the threshold.
|
|
func (vl *VolumeLayout) RecordAssign(vid needle.VolumeId, pendingDelta int64) {
|
|
vl.accessLock.Lock()
|
|
defer vl.accessLock.Unlock()
|
|
|
|
if pendingDelta > 0 {
|
|
vl.vid2size[vid] += uint64(pendingDelta)
|
|
}
|
|
if float64(vl.vid2size[vid]) > float64(vl.volumeSizeLimit)*VolumeGrowStrategy.Threshold {
|
|
vl.setVolumeCrowded(vid)
|
|
}
|
|
}
|
|
|
|
func (vl *VolumeLayout) isEmpty() bool {
|
|
vl.accessLock.RLock()
|
|
defer vl.accessLock.RUnlock()
|
|
|
|
return len(vl.vid2location) == 0
|
|
}
|
|
|
|
func (vl *VolumeLayout) Lookup(vid needle.VolumeId) []*DataNode {
|
|
vl.accessLock.RLock()
|
|
defer vl.accessLock.RUnlock()
|
|
|
|
if location := vl.vid2location[vid]; location != nil {
|
|
return location.list
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (vl *VolumeLayout) ListVolumeServers() (nodes []*DataNode) {
|
|
vl.accessLock.RLock()
|
|
defer vl.accessLock.RUnlock()
|
|
|
|
for _, location := range vl.vid2location {
|
|
nodes = append(nodes, location.list...)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (vl *VolumeLayout) PickForWrite(count uint64, option *VolumeGrowOption) (vid needle.VolumeId, counter uint64, locationList *VolumeLocationList, shouldGrow bool, err error) {
|
|
vl.accessLock.RLock()
|
|
defer vl.accessLock.RUnlock()
|
|
|
|
lenWriters := len(vl.writables)
|
|
if lenWriters <= 0 {
|
|
return 0, 0, nil, true, fmt.Errorf("%s", NoWritableVolumes)
|
|
}
|
|
if option.DataCenter == "" && option.Rack == "" && option.DataNode == "" {
|
|
vid, locationList = vl.pickWeightedByRemaining(vl.writables)
|
|
if locationList == nil || len(locationList.list) == 0 {
|
|
return 0, 0, nil, false, fmt.Errorf("Strangely vid %s is on no machine!", vid.String())
|
|
}
|
|
return vid, count, locationList.Copy(), false, nil
|
|
}
|
|
|
|
// Scan from a random offset to collect up to pickSampleSize matching
|
|
// candidates, avoiding a full scan + allocation in the common case.
|
|
var sample [pickSampleSize]needle.VolumeId
|
|
found := 0
|
|
start := rand.IntN(lenWriters)
|
|
for i := 0; i < lenWriters && found < pickSampleSize; i++ {
|
|
writableVolumeId := vl.writables[(start+i)%lenWriters]
|
|
volumeLocationList := vl.vid2location[writableVolumeId]
|
|
for _, dn := range volumeLocationList.list {
|
|
if option.DataCenter != "" && dn.GetDataCenter().Id() != NodeId(option.DataCenter) {
|
|
continue
|
|
}
|
|
if option.Rack != "" && dn.GetRack().Id() != NodeId(option.Rack) {
|
|
continue
|
|
}
|
|
if option.DataNode != "" && dn.Id() != NodeId(option.DataNode) {
|
|
continue
|
|
}
|
|
sample[found] = writableVolumeId
|
|
found++
|
|
break
|
|
}
|
|
}
|
|
if found == 0 {
|
|
return vid, count, locationList, true, fmt.Errorf("%s in DataCenter:%v Rack:%v DataNode:%v", NoWritableVolumes, option.DataCenter, option.Rack, option.DataNode)
|
|
}
|
|
vid, locationList = vl.weightedPick(sample[:found])
|
|
return vid, count, locationList.Copy(), false, nil
|
|
}
|
|
|
|
// pickSampleSize is how many random candidates to sample before doing a
|
|
// weighted pick. Keeps cost O(1) regardless of total writable volume count
|
|
// while still biasing toward emptier volumes.
|
|
const pickSampleSize = 3
|
|
|
|
// pickWeightedByRemaining randomly samples a few candidates from the list,
|
|
// then does a weighted pick among them by remaining capacity.
|
|
// Sampled candidates may repeat when len(candidates) is small relative to
|
|
// pickSampleSize; this is harmless — a repeated volume just gets proportionally
|
|
// more weight, which is a negligible statistical effect.
|
|
func (vl *VolumeLayout) pickWeightedByRemaining(candidates []needle.VolumeId) (needle.VolumeId, *VolumeLocationList) {
|
|
n := len(candidates)
|
|
if n <= pickSampleSize {
|
|
return vl.weightedPick(candidates)
|
|
}
|
|
|
|
var sample [pickSampleSize]needle.VolumeId
|
|
for i := range sample {
|
|
sample[i] = candidates[rand.IntN(n)]
|
|
}
|
|
return vl.weightedPick(sample[:])
|
|
}
|
|
|
|
func (vl *VolumeLayout) weightedPick(candidates []needle.VolumeId) (needle.VolumeId, *VolumeLocationList) {
|
|
if len(candidates) == 1 {
|
|
vid := candidates[0]
|
|
return vid, vl.vid2location[vid]
|
|
}
|
|
|
|
// first pass: sum weights
|
|
var totalRemaining uint64
|
|
for _, vid := range candidates {
|
|
totalRemaining += vl.remainingSize(vid)
|
|
}
|
|
|
|
// second pass: weighted random pick
|
|
pick := rand.Uint64N(totalRemaining)
|
|
var cumulative uint64
|
|
for _, vid := range candidates {
|
|
cumulative += vl.remainingSize(vid)
|
|
if pick < cumulative {
|
|
return vid, vl.vid2location[vid]
|
|
}
|
|
}
|
|
|
|
vid := candidates[0]
|
|
return vid, vl.vid2location[vid]
|
|
}
|
|
|
|
func (vl *VolumeLayout) remainingSize(vid needle.VolumeId) uint64 {
|
|
size := vl.vid2size[vid]
|
|
if size < vl.volumeSizeLimit {
|
|
if r := vl.volumeSizeLimit - size; r > 1 {
|
|
return r
|
|
}
|
|
}
|
|
return 1
|
|
}
|
|
|
|
func (vl *VolumeLayout) HasGrowRequest() bool {
|
|
return vl.growRequest.Load()
|
|
}
|
|
func (vl *VolumeLayout) AddGrowRequest() {
|
|
vl.growRequest.Store(true)
|
|
}
|
|
func (vl *VolumeLayout) DoneGrowRequest() {
|
|
vl.growRequest.Store(false)
|
|
}
|
|
|
|
func (vl *VolumeLayout) SetLastGrowCount(count uint32) {
|
|
if vl.lastGrowCount.Load() != count && count != 0 {
|
|
vl.lastGrowCount.Store(count)
|
|
}
|
|
}
|
|
|
|
func (vl *VolumeLayout) GetLastGrowCount() uint32 {
|
|
return vl.lastGrowCount.Load()
|
|
}
|
|
|
|
func (vl *VolumeLayout) ShouldGrowVolumes() bool {
|
|
writable, crowded := vl.GetWritableVolumeCount()
|
|
return writable <= crowded
|
|
}
|
|
|
|
func (vl *VolumeLayout) ShouldGrowVolumesByDcAndRack(writables *[]needle.VolumeId, dcId NodeId, rackId NodeId) bool {
|
|
// When replication spans multiple racks (DiffRackCount > 0), a writable
|
|
// volume's replicas only cover some racks in a DC. It is wrong to
|
|
// require every rack to host a replica — that would create volumes
|
|
// endlessly in any DC with more racks than the copy count.
|
|
// Instead, check at the DC level: if the DC already has a non-crowded
|
|
// writable volume, no growth is needed for uncovered racks.
|
|
checkDcOnly := vl.rp.DiffRackCount > 0
|
|
for _, v := range *writables {
|
|
for _, dn := range vl.Lookup(v) {
|
|
if dn.GetDataCenter().Id() != dcId {
|
|
continue
|
|
}
|
|
if !checkDcOnly && dn.GetRack().Id() != rackId {
|
|
continue
|
|
}
|
|
if _, err := dn.GetVolumesById(v); err == nil {
|
|
vl.accessLock.RLock()
|
|
size := vl.vid2size[v]
|
|
vl.accessLock.RUnlock()
|
|
if float64(size) <= float64(vl.volumeSizeLimit)*VolumeGrowStrategy.Threshold {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (vl *VolumeLayout) GetWritableVolumeCount() (active, crowded int) {
|
|
vl.accessLock.RLock()
|
|
defer vl.accessLock.RUnlock()
|
|
return len(vl.writables), len(vl.crowded)
|
|
}
|
|
|
|
func (vl *VolumeLayout) CloneWritableVolumes() (writables []needle.VolumeId) {
|
|
vl.accessLock.RLock()
|
|
writables = make([]needle.VolumeId, len(vl.writables))
|
|
copy(writables, vl.writables)
|
|
vl.accessLock.RUnlock()
|
|
return writables
|
|
}
|
|
|
|
func (vl *VolumeLayout) removeFromWritable(vid needle.VolumeId) bool {
|
|
toDeleteIndex := -1
|
|
for k, id := range vl.writables {
|
|
if id == vid {
|
|
toDeleteIndex = k
|
|
break
|
|
}
|
|
}
|
|
if toDeleteIndex >= 0 {
|
|
glog.V(0).Infoln("Volume", vid, "becomes unwritable")
|
|
vl.writables = append(vl.writables[0:toDeleteIndex], vl.writables[toDeleteIndex+1:]...)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
func (vl *VolumeLayout) setVolumeWritable(vid needle.VolumeId) bool {
|
|
for _, v := range vl.writables {
|
|
if v == vid {
|
|
return false
|
|
}
|
|
}
|
|
glog.V(1).Infoln("Volume", vid, "becomes writable")
|
|
vl.writables = append(vl.writables, vid)
|
|
return true
|
|
}
|
|
|
|
func (vl *VolumeLayout) SetVolumeReadOnly(dn *DataNode, vid needle.VolumeId) bool {
|
|
vl.accessLock.Lock()
|
|
defer vl.accessLock.Unlock()
|
|
|
|
if _, ok := vl.vid2location[vid]; ok {
|
|
vl.readonlyVolumes.Add(vid, dn)
|
|
return vl.removeFromWritable(vid)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (vl *VolumeLayout) SetVolumeWritable(dn *DataNode, vid needle.VolumeId) bool {
|
|
vl.accessLock.Lock()
|
|
defer vl.accessLock.Unlock()
|
|
|
|
if _, ok := vl.vid2location[vid]; ok {
|
|
vl.readonlyVolumes.Remove(vid, dn)
|
|
}
|
|
|
|
if vl.enoughCopies(vid) {
|
|
return vl.setVolumeWritable(vid)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (vl *VolumeLayout) SetVolumeUnavailable(dn *DataNode, vid needle.VolumeId) bool {
|
|
vl.accessLock.Lock()
|
|
defer vl.accessLock.Unlock()
|
|
|
|
if location, ok := vl.vid2location[vid]; ok {
|
|
if location.Remove(dn) {
|
|
vl.readonlyVolumes.Remove(vid, dn)
|
|
vl.oversizedVolumes.Remove(vid, dn)
|
|
if location.Length() < vl.rp.GetCopyCount() {
|
|
glog.V(0).Infoln("Volume", vid, "has", location.Length(), "replica, less than required", vl.rp.GetCopyCount())
|
|
return vl.removeFromWritable(vid)
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
func (vl *VolumeLayout) SetVolumeAvailable(dn *DataNode, vid needle.VolumeId, isReadOnly, isFullCapacity bool) bool {
|
|
vl.accessLock.Lock()
|
|
defer vl.accessLock.Unlock()
|
|
|
|
vInfo, err := dn.GetVolumesById(vid)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
vl.vid2location[vid].Set(dn)
|
|
|
|
if vInfo.ReadOnly || isReadOnly || isFullCapacity {
|
|
return false
|
|
}
|
|
|
|
if vl.enoughCopies(vid) {
|
|
return vl.setVolumeWritable(vid)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (vl *VolumeLayout) enoughCopies(vid needle.VolumeId) bool {
|
|
locations := vl.vid2location[vid].Length()
|
|
desired := vl.rp.GetCopyCount()
|
|
return locations == desired || (vl.replicationAsMin && locations > desired)
|
|
}
|
|
|
|
func (vl *VolumeLayout) SetVolumeCapacityFull(vid needle.VolumeId) bool {
|
|
vl.accessLock.Lock()
|
|
defer vl.accessLock.Unlock()
|
|
|
|
wasWritable := vl.removeFromWritable(vid)
|
|
if wasWritable {
|
|
glog.V(0).Infof("Volume %d reaches full capacity.", vid)
|
|
}
|
|
return wasWritable
|
|
}
|
|
|
|
func (vl *VolumeLayout) removeFromCrowded(vid needle.VolumeId) {
|
|
if _, ok := vl.crowded[vid]; ok {
|
|
glog.V(0).Infoln("Volume", vid, "becomes uncrowded")
|
|
delete(vl.crowded, vid)
|
|
}
|
|
}
|
|
|
|
func (vl *VolumeLayout) setVolumeCrowded(vid needle.VolumeId) {
|
|
if _, ok := vl.crowded[vid]; !ok {
|
|
vl.crowded[vid] = struct{}{}
|
|
glog.V(0).Infoln("Volume", vid, "becomes crowded")
|
|
}
|
|
}
|
|
|
|
func (vl *VolumeLayout) SetVolumeCrowded(vid needle.VolumeId) {
|
|
// since delete is guarded by accessLock.Lock(),
|
|
// and is always called in sequential order,
|
|
// RLock() should be safe enough
|
|
vl.accessLock.RLock()
|
|
defer vl.accessLock.RUnlock()
|
|
|
|
vl.setVolumeCrowded(vid)
|
|
}
|
|
|
|
type VolumeLayoutInfo struct {
|
|
Replication string `json:"replication"`
|
|
TTL string `json:"ttl"`
|
|
Writables []needle.VolumeId `json:"writables"`
|
|
Collection string `json:"collection"`
|
|
DiskType string `json:"diskType"`
|
|
}
|
|
|
|
func (vl *VolumeLayout) ToInfo() (info VolumeLayoutInfo) {
|
|
info.Replication = vl.rp.String()
|
|
info.TTL = vl.ttl.String()
|
|
info.Writables = vl.writables
|
|
info.DiskType = vl.diskType.ReadableString()
|
|
//m["locations"] = vl.vid2location
|
|
return
|
|
}
|
|
|
|
func (vlc *VolumeLayoutCollection) ToVolumeGrowRequest() *master_pb.VolumeGrowRequest {
|
|
return &master_pb.VolumeGrowRequest{
|
|
Collection: vlc.Collection,
|
|
Replication: vlc.VolumeLayout.rp.String(),
|
|
Ttl: vlc.VolumeLayout.ttl.String(),
|
|
DiskType: vlc.VolumeLayout.diskType.String(),
|
|
}
|
|
}
|
|
|
|
func (vl *VolumeLayout) Stats() *VolumeLayoutStats {
|
|
vl.accessLock.RLock()
|
|
defer vl.accessLock.RUnlock()
|
|
|
|
ret := &VolumeLayoutStats{}
|
|
|
|
freshThreshold := time.Now().Unix() - 60
|
|
|
|
for vid, vll := range vl.vid2location {
|
|
size, fileCount := vll.Stats(vid, freshThreshold)
|
|
ret.FileCount += uint64(fileCount)
|
|
ret.UsedSize += size * uint64(vll.Length())
|
|
if vl.readonlyVolumes.IsTrue(vid) {
|
|
ret.TotalSize += size * uint64(vll.Length())
|
|
} else {
|
|
ret.TotalSize += vl.volumeSizeLimit * uint64(vll.Length())
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|