Files
seaweedfs/weed/topology/topology.go
T
Chris LuandDevin eec2452e30 master: replicate the recovery-wait growth unfreeze through raft (#12165)
* master: replicate the recovery-wait growth unfreeze through raft

state_loss_recovery_wait lifted the volume-growth freeze only in
memory, while the freeze itself was replicated. A restart replayed the
frozen flag and growth stayed refused until the wait expired again, on
every restart, unless an operator ran volume.growth.resume.

The lift now applies GrowthFrozen=false like ResumeVolumeGrowth, from a
goroutine since IsGrowthFrozen is a read path that cannot block on
raft. The apply runs under reportedTopologyIdsLock so a freeze landing
after the lift keeps its command last in the log; the stale unfreeze
skips itself when the flag is frozen again.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* master: cover raft replication of the recovery-wait unfreeze

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* master: retry and re-arm a growth unfreeze raft failed to record

applyGrowthFreeze swallowed the raft error, so a rejected lift during a
leader change left the flag clear while the log still held the freeze;
nothing retried and a restart re-froze growth anyway. The apply now
returns the error, replicateGrowthUnfreeze retries it, and on
persistent failure re-arms the flag so the next IsGrowthFrozen
attempts the lift again instead of diverging from the log.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* master: release the topology-id lock before the freeze raft write

CheckReportedTopologyId and SetTopologyInitPending held
reportedTopologyIdsLock across the blocking raft apply, so a slow
apply stalled every later report — including the heartbeat that
should trigger the next freeze. The lock now covers only the
mismatch decision; the replicate step runs after it is released.

* master: bind the recovery unfreeze to the freeze instant it lifts

An unfreeze submitted while a newer freeze sat committed but not yet
applied — e.g. after this node won an election — could land behind
that freeze in the log and erase it. The command now carries the
freeze instant cluster-wide (GrowthFrozenAt) and the lift is bound
to the instant its submitter observed (GrowthUnfreezeFor): the state
machine no-ops a lift while a strictly newer freeze exists, which
also lets the unfreeze run without holding the topology-id lock at
all. Concurrent freeze writes keep their own mutex so log order
cannot regress the instant.

* master: a superseded unfreeze retry leaves the resumed flag alone

replicateGrowthUnfreeze broke out of its retry loop when a newer\nfreeze superseded the lift, but the break fell through to the\nre-arm: an operator resume in between was silently undone. Return\ninstead — the newer freeze generation owns the flag.

* master: replayed freeze entries without an instant stay ageless

Freeze commands written before the instant field existed carry zero.\nSubstituting the replay time made each restart stamp the freeze as\nnew, so a committed bound unfreeze always looked older and was\nrejected — the freeze came back on every restart. Keep the zero\ninstant: the recovery window reads as elapsed and bound lifts apply.

* master: pack the growth freeze flag and generation into one atomic

Separate flag and timestamp atomics left three holes: a resume's\nload-and-clear could lift a generation stamped in between, a bound\nlift compared stamps numerically although masters' clocks need not\nbe monotonic, and an ageless generation had no identity a bound lift\ncould match without colliding with the unconditional encoding.\n\nOne value now carries the state — 0 unfrozen, -1 an ageless freeze,\nelse the freeze instant — so every transition is a single atomic\nswap or compare-and-swap bound to an exact generation.

* master: a repeat mismatch report keeps the armed freeze generation

Re-stamping while already frozen never reaches the raft log, so the local generation diverges from the replicated one and every bound unfreeze becomes a no-op on replay — the freeze returns after restart or failover.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* master: snapshots read the freeze flag and generation in one load

Save, Snapshot, and PersistMaxVolumeId read IsGrowthFrozen and the generation separately, so a resume landing between them persisted frozen=true with stamp 0 — restoring an ageless freeze a committed bound lift cannot clear.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 08:44:45 -07:00

2060 lines
70 KiB
Go

package topology
import (
"encoding/json"
"errors"
"fmt"
"math"
"math/rand/v2"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
backoff "github.com/cenkalti/backoff/v4"
hashicorpRaft "github.com/hashicorp/raft"
"github.com/seaweedfs/raft"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/util"
)
const (
// WarmupPulseMultiplier is the number of heartbeat intervals to wait after
// a leader change before treating volume lookup misses as definitive.
WarmupPulseMultiplier = 3
)
type Topology struct {
vacuumLockCounter int64
NodeImpl
collectionMap *util.ConcurrentReadMap
ecShardMap map[needle.VolumeId]*EcShardLocations
ecShardMapLock sync.RWMutex
pulse int64
volumeSizeLimit uint64
replicationAsMin bool
maxWritableVolumes int
vacuumDisabledByOperator atomic.Bool // true when operator manually disables vacuum
vacuumDisabledByPlugin atomic.Bool // true when disabled by the vacuum plugin monitor
adminServerConnectedFunc func() bool // optional callback to check admin server presence
Sequence sequence.Sequencer
chanFullVolumes chan storage.VolumeInfo
chanCrowdedVolumes chan storage.VolumeInfo
Configuration *Configuration
RaftServer raft.Server
RaftServerAccessLock sync.RWMutex
HashicorpRaft *hashicorpRaft.Raft
barrierLock sync.Mutex
barrierDone bool
UuidAccessLock sync.RWMutex
UuidMap map[string][]string
topologyId string
topologyIdLock sync.RWMutex
lastLeaderChangeTime time.Time
hadVolumesAtLeaderChange bool
lastLeaderChangeTimeLock sync.RWMutex
// dataNodeIndex is an address -> *DataNode lookup so callers (e.g. the
// Ping admission gate) do not have to walk every dc/rack/node tier on
// every request. Keys use the canonical http form returned by
// pb.ServerAddress.ToHttpAddress so a target like "1.2.3.4:8080" finds
// the same node whether or not the grpc port suffix is present.
dataNodeIndex map[string]*DataNode
dataNodeIndexLock sync.RWMutex
maxVolumeIdFile string
maxVolumeIdFileLock sync.Mutex
lastPersistedMaxVolumeId atomic.Uint32
lastPersistedTopologyId atomic.Value
lastPersistedGrowthFrozen atomic.Bool
// growthFrozenState packs the freeze flag and its generation into one
// value: 0 = unfrozen, -1 = frozen without a recorded instant (entries
// written before the field existed), >0 = frozen at that instant. Every
// transition is a single atomic swap or compare-and-swap, so a lift can
// only clear the exact generation it was bound to — a stale unfreeze
// cannot erase a newer freeze, a resume cannot lift a generation it did
// not observe, and stamps need not be monotonic across masters.
growthFrozenState atomic.Int64
metaReadOnly atomic.Bool
freezeOnMismatch atomic.Bool
stateLossRecoveryWait atomic.Int64
allocationNotBefore atomic.Int64
allocationEvidenceSince atomic.Int64
allocationDeferral atomic.Int64
topologyInitPending atomic.Bool
// freshMinted marks that this boot minted the TopologyId instead of
// recovering one: the master's persisted state was empty, so no cluster
// member registered under a prior control-plane instance.
freshMinted atomic.Bool
// releaseAllocOnReport lifts a post-mint allocation hold as soon as a full
// volume-server inventory has been applied, for setups with a known
// reporter (weed mini, single-master weed server) whose embedded volume
// server reports the full inventory in its first heartbeat. singleReporter
// additionally refuses every other reporter, so its report drops the hold
// outright; expectedReporter only gates which report may lift the hold,
// and the lift leaves a confirm pulse for reporters still admitted.
// inventoryApplied records that the expected report has been synced into
// this topology.
releaseAllocOnReport atomic.Bool
inventoryApplied atomic.Bool
singleReporter atomic.Value // string
expectedReporter atomic.Value // string
reportedTopologyIdsLock sync.Mutex
pendingReportedTopologyIds map[string]struct{}
// growthFreezeApplyLock serializes replicated freeze writes so the log
// order of concurrent mismatch reports cannot land an older freeze
// instant after a newer one. Unfreezes skip it: the state machine
// applies a lift only against the freeze generation it targeted.
growthFreezeApplyLock sync.Mutex
// quarantinedVolumeIds are (node, vid) pairs force-read-only because the
// vid collided across layouts. Kept separate from the reported flag so
// later heartbeats from the same server cannot quietly undo it.
quarantinedVolumeIds sync.Map
// quarantineLock serializes each quarantine record update with the layout
// mark it justifies: a release pass and a heartbeat read-only flag update
// each observe record and mark as one state, never a mark whose record
// was already deleted or a cleared mark whose record is not yet visible.
quarantineLock sync.Mutex
// quarantinePushLocks serialize the remote mark/unmark calls per replica
// so a push decided under an older record state cannot land on the
// volume server after a newer one. Keyed by replica so a slow server
// does not stall pushes to the others.
quarantinePushLocks map[quarantinedVolume]*quarantinePushLock
quarantinePushLocksMu sync.Mutex
// duplicateFlagChecks tracks in-flight flagDuplicateVolumeId passes so
// tests can wait for the async checks a registration started.
duplicateFlagChecks sync.WaitGroup
// QuarantineVolumeFn pushes the quarantine onto the volume server itself
// (VolumeMarkReadonly/VolumeMarkWritable). nil keeps quarantine at the
// layout level only.
QuarantineVolumeFn func(dn *DataNode, vid needle.VolumeId, readOnly bool) error
}
type quarantinedVolume struct {
dnId NodeId
vid needle.VolumeId
}
type quarantinedVolumeState struct {
reported bool
enforced bool
retry bool
}
type quarantinePushLock struct {
sync.Mutex
refs int
}
func (t *Topology) isQuarantined(dn *DataNode, vid needle.VolumeId) bool {
_, ok := t.quarantinedVolumeIds.Load(quarantinedVolume{dn.Id(), vid})
return ok
}
// pushQuarantine sends the remote mark only while it still matches the
// record: a read-only push requires the pair to exist, a writable push
// requires it to be gone. The bool reports whether the remote call was
// actually made, so callers can tell a skipped stale push from a failed one.
func (t *Topology) pushQuarantine(dn *DataNode, vid needle.VolumeId, readOnly bool) (bool, error) {
unlock := t.lockQuarantinePush(quarantinedVolume{dn.Id(), vid})
defer unlock()
return t.sendQuarantine(dn, vid, readOnly)
}
// sendQuarantine runs the record check and remote mark; the caller holds
// the replica's push lock.
func (t *Topology) sendQuarantine(dn *DataNode, vid needle.VolumeId, readOnly bool) (bool, error) {
t.quarantineLock.Lock()
_, exists := t.quarantinedVolumeIds.Load(quarantinedVolume{dn.Id(), vid})
t.quarantineLock.Unlock()
if exists != readOnly {
return false, nil
}
return true, t.QuarantineVolumeFn(dn, vid, readOnly)
}
// releaseQuarantineWritable pushes the writable mark and re-arms the record
// on failure, all under the replica's push lock so a queued stale push cannot
// slip between the failure and the restore and undo the re-armed record.
func (t *Topology) releaseQuarantineWritable(dn *DataNode, vid needle.VolumeId) {
unlock := t.lockQuarantinePush(quarantinedVolume{dn.Id(), vid})
defer unlock()
if _, err := t.sendQuarantine(dn, vid, false); err != nil {
t.restoreQuarantineRecord(dn, vid)
}
}
// lockQuarantinePush returns the release for q's push lock, creating the lock
// on first use and dropping it once the last holder lets go.
func (t *Topology) lockQuarantinePush(q quarantinedVolume) func() {
t.quarantinePushLocksMu.Lock()
if t.quarantinePushLocks == nil {
t.quarantinePushLocks = make(map[quarantinedVolume]*quarantinePushLock)
}
l, ok := t.quarantinePushLocks[q]
if !ok {
l = &quarantinePushLock{}
t.quarantinePushLocks[q] = l
}
l.refs++
t.quarantinePushLocksMu.Unlock()
l.Lock()
return func() {
t.quarantinePushLocksMu.Lock()
l.refs--
if l.refs == 0 {
delete(t.quarantinePushLocks, q)
}
t.quarantinePushLocksMu.Unlock()
l.Unlock()
}
}
// takeQuarantineRetryLocked disarms a pending release retry and reports
// whether one was armed. The caller holds quarantineLock.
func (t *Topology) takeQuarantineRetryLocked(q quarantinedVolume) bool {
v, ok := t.quarantinedVolumeIds.Load(q)
if !ok {
return false
}
state := v.(quarantinedVolumeState)
if !state.retry {
return false
}
state.retry = false
t.quarantinedVolumeIds.Store(q, state)
return true
}
// retryPendingQuarantineReleases re-runs the release pass for every record a
// failed writable push re-armed. The retry in enforceQuarantine only fires
// while a heartbeat keeps carrying the volume, and an idle replica drops out
// of the delta reports once its stored flag matches the server's, so pending
// retries also run off the leader pulse.
func (t *Topology) retryPendingQuarantineReleases() {
vids := make(map[needle.VolumeId]struct{})
t.quarantineLock.Lock()
t.quarantinedVolumeIds.Range(func(key, _ any) bool {
q := key.(quarantinedVolume)
if t.takeQuarantineRetryLocked(q) {
vids[q.vid] = struct{}{}
}
return true
})
t.quarantineLock.Unlock()
for vid := range vids {
go t.releaseResolvedQuarantine(vid)
}
}
// enforceQuarantine keeps the latest reported read-only flag for a quarantined
// copy so a later release restores the server's true state. The stored report
// keeps the server's flag: quarantine lives at the layout level, and folding
// it into the record here would make the master's volume digest permanently
// disagree with the server's and force a full volume list on every heartbeat.
func (t *Topology) enforceQuarantine(dn *DataNode, vi storage.VolumeInfo) storage.VolumeInfo {
q := quarantinedVolume{dn.Id(), vi.Id}
t.quarantineLock.Lock()
v, ok := t.quarantinedVolumeIds.Load(q)
if !ok {
t.quarantineLock.Unlock()
return vi
}
if t.takeQuarantineRetryLocked(q) {
// A failed writable push re-armed this record; hand the release pass
// one retry per report. Clearing it first keeps a surviving conflict
// on the normal enforce path afterwards.
t.quarantineLock.Unlock()
go t.releaseResolvedQuarantine(vi.Id)
return vi
}
state := v.(quarantinedVolumeState)
if !state.enforced {
state.reported = vi.ReadOnly
}
if t.QuarantineVolumeFn == nil || (state.enforced && vi.ReadOnly) {
t.quarantinedVolumeIds.Store(q, state)
t.quarantineLock.Unlock()
return vi
}
t.quarantineLock.Unlock()
// The mark never landed on the server, or the server lost it (restart,
// manual unquarantine); push it while the conflict stands. The call is
// remote, so it runs outside the lock.
pushed, err := t.pushQuarantine(dn, vi.Id, true)
if pushed && err == nil {
state.enforced = true
}
t.quarantineLock.Lock()
cur, still := t.quarantinedVolumeIds.Load(q)
if still {
latest := cur.(quarantinedVolumeState)
if !latest.enforced {
latest.reported = state.reported
}
latest.enforced = latest.enforced || state.enforced
t.quarantinedVolumeIds.Store(q, latest)
t.quarantineLock.Unlock()
return vi
}
t.quarantineLock.Unlock()
if state.enforced && !state.reported {
// The conflict resolved while the push was in flight; leave the
// server consistent with the released layout.
t.releaseQuarantineWritable(dn, vi.Id)
}
return vi
}
func NewTopology(id string, seq sequence.Sequencer, volumeSizeLimit uint64, pulse int, replicationAsMin bool) *Topology {
t := &Topology{}
t.id = NodeId(id)
t.nodeType = "Topology"
t.NodeImpl.value = t
t.diskUsages = newDiskUsages()
t.children = make(map[NodeId]Node)
t.capacityReservations = newCapacityReservations()
t.collectionMap = util.NewConcurrentReadMap()
t.ecShardMap = make(map[needle.VolumeId]*EcShardLocations)
t.pulse = int64(pulse)
t.volumeSizeLimit = volumeSizeLimit
t.replicationAsMin = replicationAsMin
t.Sequence = seq
t.chanFullVolumes = make(chan storage.VolumeInfo)
t.chanCrowdedVolumes = make(chan storage.VolumeInfo)
t.allocationDeferral.Store(-1)
t.Configuration = &Configuration{}
t.dataNodeIndex = make(map[string]*DataNode)
t.quarantinePushLocks = make(map[quarantinedVolume]*quarantinePushLock)
return t
}
// LookupDataNodeByAddress returns the registered DataNode that serves addr,
// or nil if no such node has been observed. Lookup is O(1) and uses the
// canonical http form of the address so callers that pass either
// "host:port" or "host:port.grpc" find the same node.
func (t *Topology) LookupDataNodeByAddress(addr pb.ServerAddress) *DataNode {
if addr == "" {
return nil
}
t.dataNodeIndexLock.RLock()
defer t.dataNodeIndexLock.RUnlock()
if t.dataNodeIndex == nil {
return nil
}
return t.dataNodeIndex[addr.ToHttpAddress()]
}
// registerDataNodeAddress records dn in the address index under its current
// http address. Callers must invoke unregisterDataNodeAddress with the prior
// address whenever a node's Ip or Port changes (e.g. k8s pod reschedule).
func (t *Topology) registerDataNodeAddress(dn *DataNode) {
if dn == nil {
return
}
key := dn.ServerAddress().ToHttpAddress()
if key == "" {
return
}
t.dataNodeIndexLock.Lock()
defer t.dataNodeIndexLock.Unlock()
if t.dataNodeIndex == nil {
t.dataNodeIndex = make(map[string]*DataNode)
}
t.dataNodeIndex[key] = dn
}
// unregisterDataNodeAddress removes the index entry for addr, but only when
// the entry still points at dn. The conditional guard avoids dropping a
// freshly re-registered node whose address happens to alias the one being
// removed (e.g. legacy id transitions or a fast restart).
func (t *Topology) unregisterDataNodeAddress(addr pb.ServerAddress, dn *DataNode) {
if addr == "" {
return
}
key := addr.ToHttpAddress()
if key == "" {
return
}
t.dataNodeIndexLock.Lock()
defer t.dataNodeIndexLock.Unlock()
if existing, ok := t.dataNodeIndex[key]; ok && (dn == nil || existing == dn) {
delete(t.dataNodeIndex, key)
}
}
// FreeBytes sums what every volume server reports as free on its filesystems.
// reported is false unless all of them answered: the one that stayed quiet may
// be the one holding the room, and a partial sum would read as a cluster with
// none left.
func (t *Topology) FreeBytes() (freeBytes uint64, reported bool) {
for _, dcNode := range t.Children() {
for _, rackNode := range dcNode.Children() {
for _, dataNode := range rackNode.Children() {
nodeFreeBytes, nodeReported := dataNode.GetDiskUsages().FreeBytes()
if !nodeReported {
return 0, false
}
freeBytes += nodeFreeBytes
}
}
}
return freeBytes, true
}
func (t *Topology) IsChildLocked() (bool, error) {
if t.IsLocked() {
return true, errors.New("topology is locked")
}
for _, dcNode := range t.Children() {
if dcNode.IsLocked() {
return true, fmt.Errorf("topology child %s is locked", dcNode.String())
}
for _, rackNode := range dcNode.Children() {
if rackNode.IsLocked() {
return true, fmt.Errorf("dc %s child %s is locked", dcNode.String(), rackNode.String())
}
for _, dataNode := range rackNode.Children() {
if dataNode.IsLocked() {
return true, fmt.Errorf("rack %s child %s is locked", rackNode.String(), dataNode.Id())
}
}
}
}
return false, nil
}
// SetLastLeaderChangeTime records the time of the most recent leader transition.
// It also snapshots whether the topology already had known volumes at that
// moment. IsWarmingUp uses the snapshot instead of the live MaxVolumeId so a
// fresh cluster that happens to grow its first volume inside the warmup window
// does not retroactively flip into "warming up" state — there is no prior
// topology to wait for on a bootstrap.
func (t *Topology) SetLastLeaderChangeTime(ts time.Time) {
hadVolumes := t.GetMaxVolumeId() > 0
t.lastLeaderChangeTimeLock.Lock()
defer t.lastLeaderChangeTimeLock.Unlock()
t.lastLeaderChangeTime = ts
t.hadVolumesAtLeaderChange = hadVolumes
}
// GetLastLeaderChangeTime returns the time of the most recent leader transition.
func (t *Topology) GetLastLeaderChangeTime() time.Time {
t.lastLeaderChangeTimeLock.RLock()
defer t.lastLeaderChangeTimeLock.RUnlock()
return t.lastLeaderChangeTime
}
// IsWarmingUp returns true if the master recently became leader and may not yet
// have a complete topology. After a leader change or restart, volume servers need
// up to WarmupPulseMultiplier heartbeat intervals to reconnect and report their volumes.
// Returns false on a fresh cluster start — i.e. when no volumes existed at the
// time of the leader change — since there is no prior topology state to wait for.
// Checking the *live* MaxVolumeId here would make a bootstrapping cluster flip
// into warming-up the moment its first volume is grown, which manifested as a
// 15-second window of spurious Unavailable errors on AssignVolume for workloads
// that start writing immediately (see #8777).
func (t *Topology) IsWarmingUp() bool {
t.lastLeaderChangeTimeLock.RLock()
lastChange := t.lastLeaderChangeTime
hadVolumes := t.hadVolumesAtLeaderChange
t.lastLeaderChangeTimeLock.RUnlock()
if !hadVolumes || lastChange.IsZero() {
return false
}
return time.Since(lastChange) < t.WarmupDuration()
}
// WarmupDuration returns the configured warmup duration based on pulse interval.
func (t *Topology) WarmupDuration() time.Duration {
return time.Duration(t.pulse*WarmupPulseMultiplier) * time.Second
}
// RemainingWarmupDuration returns how much warmup time is left, or 0 if not warming up.
func (t *Topology) RemainingWarmupDuration() time.Duration {
if !t.IsWarmingUp() {
return 0
}
remaining := t.WarmupDuration() - time.Since(t.GetLastLeaderChangeTime())
if remaining < 0 {
return 0
}
return remaining
}
func (t *Topology) IsLeader() bool {
t.RaftServerAccessLock.RLock()
defer t.RaftServerAccessLock.RUnlock()
if t.RaftServer != nil {
if t.RaftServer.State() == raft.Leader {
return true
}
// Directly check leader to avoid re-acquiring lock via MaybeLeader()
leader := pb.ServerAddress(t.RaftServer.Leader())
if leader != "" {
if pb.ServerAddress(t.RaftServer.Name()).Equals(leader) {
return true
}
}
} else if t.HashicorpRaft != nil {
if t.HashicorpRaft.State() == hashicorpRaft.Leader {
return true
}
}
return false
}
func (t *Topology) IsLeaderAndCanRead() bool {
if t.RaftServer != nil {
return t.IsLeader()
} else if t.HashicorpRaft != nil {
return t.IsLeader() && t.DoBarrier()
} else {
return false
}
}
func (t *Topology) DoBarrier() bool {
t.barrierLock.Lock()
defer t.barrierLock.Unlock()
if t.barrierDone {
return true
}
glog.V(0).Infof("raft do barrier")
barrier := t.HashicorpRaft.Barrier(2 * time.Minute)
if err := barrier.Error(); err != nil {
glog.Errorf("failed to wait for barrier, error %s", err)
return false
}
t.barrierDone = true
glog.V(0).Infof("raft do barrier success")
return true
}
func (t *Topology) BarrierReset() {
t.barrierLock.Lock()
defer t.barrierLock.Unlock()
t.barrierDone = false
}
func (t *Topology) Leader() (l pb.ServerAddress, err error) {
exponentialBackoff := backoff.NewExponentialBackOff()
exponentialBackoff.InitialInterval = 100 * time.Millisecond
exponentialBackoff.MaxElapsedTime = 20 * time.Second
leaderNotSelected := errors.New("leader not selected yet")
l, err = backoff.RetryWithData(
func() (l pb.ServerAddress, err error) {
l, err = t.MaybeLeader()
if err == nil && l == "" {
err = leaderNotSelected
}
return l, err
},
exponentialBackoff)
if err == leaderNotSelected {
l = ""
}
return l, err
}
func (t *Topology) MaybeLeader() (l pb.ServerAddress, err error) {
t.RaftServerAccessLock.RLock()
defer t.RaftServerAccessLock.RUnlock()
if t.RaftServer != nil {
l = pb.ServerAddress(t.RaftServer.Leader())
if l == "" && t.RaftServer.State() == raft.Leader {
l = pb.ServerAddress(t.RaftServer.Name())
}
} else if t.HashicorpRaft != nil {
l = pb.ServerAddress(t.HashicorpRaft.Leader())
} else {
err = errors.New("Raft Server not ready yet!")
}
return
}
func (t *Topology) Lookup(collection string, vid needle.VolumeId) (dataNodes []*DataNode) {
// maybe an issue if lots of collections?
if collection == "" {
for _, c := range t.collectionMap.Items() {
if list := c.(*Collection).Lookup(vid); list != nil {
return list
}
}
} else {
if c, ok := t.collectionMap.Find(collection); ok {
return c.(*Collection).Lookup(vid)
}
}
if locations, found := t.LookupEcShards(vid); found {
for _, loc := range locations.Locations {
dataNodes = append(dataNodes, loc...)
}
return dataNodes
}
return nil
}
// SetMetaReadOnly disables volume id allocation: -mdir is on a read-only
// filesystem and raft runs from a copy that does not survive a restart.
func (t *Topology) SetMetaReadOnly(readOnly bool) {
t.metaReadOnly.Store(readOnly)
}
func (t *Topology) NextVolumeId() (needle.VolumeId, error) {
if t.metaReadOnly.Load() {
return 0, fmt.Errorf("volume growth disabled: -mdir is on a read-only filesystem")
}
if !t.IsLeaderAndCanRead() {
return 0, fmt.Errorf("as leader can not read yet")
}
for t.topologyInitPending.Load() {
if !t.IsLeaderAndCanRead() {
return 0, fmt.Errorf("as leader can not read yet")
}
time.Sleep(100 * time.Millisecond)
}
if t.IsGrowthFrozen() {
return 0, fmt.Errorf("volume growth is frozen after master state loss; verify the cluster inventory, then run: volume.growth.resume")
}
if wait := t.allocationDelay(); wait > 0 {
glog.V(0).Infof("deferring volume id allocation for %v", wait)
}
for wait := t.allocationDelay(); wait > 0; wait = t.allocationDelay() {
if wait > 500*time.Millisecond {
wait = 500 * time.Millisecond
}
time.Sleep(wait)
if t.IsGrowthFrozen() {
return 0, fmt.Errorf("volume growth is frozen after master state loss; verify the cluster inventory, then run: volume.growth.resume")
}
}
vid := t.GetMaxVolumeId()
next := vid.Next()
if err := t.PersistMaxVolumeId(next); err != nil {
return 0, fmt.Errorf("persist max volume id %d: %w", next, err)
}
t.RaftServerAccessLock.RLock()
defer t.RaftServerAccessLock.RUnlock()
if t.RaftServer != nil {
if _, err := t.RaftServer.Do(NewMaxVolumeIdCommand(next, t.GetTopologyId())); err != nil {
return 0, err
}
} else if t.HashicorpRaft != nil {
b, err := json.Marshal(NewMaxVolumeIdCommand(next, t.GetTopologyId()))
if err != nil {
return 0, fmt.Errorf("failed marshal NewMaxVolumeIdCommand: %+v", err)
}
if future := t.HashicorpRaft.Apply(b, time.Second); future.Error() != nil {
return 0, future.Error()
}
}
return next, nil
}
// DefaultNeedleSizeEstimate is the fallback per-file-ID size estimate when
// the client does not provide an expected data size.
const DefaultNeedleSizeEstimate uint64 = 1024 * 1024 // 1 MB
func (t *Topology) PickForWrite(requestedCount uint64, option *VolumeGrowOption, volumeLayout *VolumeLayout, expectedDataSize uint64) (fileId string, count uint64, volumeLocationList *VolumeLocationList, shouldGrow bool, err error) {
var vid needle.VolumeId
vid, count, volumeLocationList, shouldGrow, err = volumeLayout.PickForWrite(requestedCount, option)
if err != nil {
return "", 0, nil, shouldGrow, fmt.Errorf("failed to find writable volumes for collection:%s replication:%s ttl:%s error: %v", option.Collection, option.ReplicaPlacement.String(), option.Ttl.String(), err)
}
if volumeLocationList == nil || volumeLocationList.Length() == 0 {
return "", 0, nil, shouldGrow, fmt.Errorf("%s available for collection:%s replication:%s ttl:%s", NoWritableVolumes, option.Collection, option.ReplicaPlacement.String(), option.Ttl.String())
}
// Track estimated assigned bytes to spread load between heartbeats. A flat
// fallback overcharges a small-file workload enough to mark near-empty
// volumes full, so prefer the volume's own average.
sizePerFile := DefaultNeedleSizeEstimate
if expectedDataSize > 0 {
sizePerFile = expectedDataSize
} else if vi, infoErr := volumeLocationList.Head().GetVolumesById(vid); infoErr == nil && vi.FileCount > 0 {
if avg := vi.Size / uint64(vi.FileCount); avg > 0 {
sizePerFile = avg
}
}
pendingBytes := min(uint64(count)*sizePerFile, uint64(math.MaxInt64))
if volumeLayout.RecordAssign(vid, int64(pendingBytes)) {
volumeLayout.AdjustActiveVolumeCountForFull(vid)
}
nextFileId := t.Sequence.NextFileId(requestedCount)
fileId = needle.NewFileId(vid, nextFileId, rand.Uint32()).String()
return fileId, count, volumeLocationList, shouldGrow, nil
}
func (t *Topology) GetVolumeLayout(collectionName string, rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType) *VolumeLayout {
return t.collectionMap.Get(collectionName, func() interface{} {
c := NewCollection(collectionName, t.volumeSizeLimit, t.replicationAsMin)
c.maxWritableVolumes = t.maxWritableVolumes
return c
}).(*Collection).GetOrCreateVolumeLayout(rp, ttl, diskType)
}
// SetMaxWritableVolumes caps how many writable volumes in a volume layout
// (collection + replica placement + ttl + disk type) assignments are spread
// over; 0 means every writable volume is a candidate. It only affects
// collections created afterwards, so call it before serving.
func (t *Topology) SetMaxWritableVolumes(n int) {
t.maxWritableVolumes = n
}
// FindVolumeLayout is the read-only counterpart of GetVolumeLayout: it returns
// nil when the collection or its layout does not exist, instead of creating
// one. Lets callers check a layout's state without polluting the collection
// map with an empty collection.
func (t *Topology) FindVolumeLayout(collectionName string, rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType) *VolumeLayout {
c, ok := t.collectionMap.Find(collectionName)
if !ok {
return nil
}
vl, ok := c.(*Collection).GetVolumeLayout(rp, ttl, diskType)
if !ok {
return nil
}
return vl
}
// DecayQuietVolumeSizes decays pending assign estimates across every layout.
// A volume that changed reports within a pulse, so two quiet pulses mean the
// size on record is the size there is.
func (t *Topology) DecayQuietVolumeSizes() {
quietCutoff := time.Duration(2*t.pulse) * time.Second
for _, c := range t.collectionMap.Items() {
for _, vl := range c.(*Collection).GetAllVolumeLayouts() {
vl.DecayQuietVolumeSizes(quietCutoff)
}
}
}
// CollectionVolumeStats aggregates stats across all volume layouts and EC
// volumes of one collection, or across every collection when collectionName is
// empty.
func (t *Topology) CollectionVolumeStats(collectionName string) *VolumeLayoutStats {
ret := &VolumeLayoutStats{}
var collections []*Collection
if collectionName == "" {
for _, c := range t.collectionMap.Items() {
collections = append(collections, c.(*Collection))
}
} else if c, found := t.FindCollection(collectionName); found {
collections = append(collections, c)
}
for _, c := range collections {
for _, vl := range c.GetAllVolumeLayouts() {
stats := vl.Stats()
ret.TotalSize += stats.TotalSize
ret.UsedSize += stats.UsedSize
ret.LogicalUsedSize += stats.LogicalUsedSize
ret.FileCount += stats.FileCount
}
}
// EC volumes live outside collectionMap, so a collection whose volumes are
// all encoded has no layout left to report them
ecStats := t.CollectionEcVolumeStats(collectionName)
ret.TotalSize += ecStats.TotalSize
ret.UsedSize += ecStats.UsedSize
ret.LogicalUsedSize += ecStats.LogicalUsedSize
ret.FileCount += ecStats.FileCount
return ret
}
func (t *Topology) ListCollections(includeNormalVolumes, includeEcVolumes bool) (ret []string) {
found := make(map[string]bool)
if includeNormalVolumes {
t.collectionMap.RLock()
for _, c := range t.collectionMap.Items() {
found[c.(*Collection).Name] = true
}
t.collectionMap.RUnlock()
}
if includeEcVolumes {
t.ecShardMapLock.RLock()
for _, ecVolumeLocation := range t.ecShardMap {
found[ecVolumeLocation.Collection] = true
}
t.ecShardMapLock.RUnlock()
}
for k := range found {
ret = append(ret, k)
}
slices.Sort(ret)
return ret
}
func (t *Topology) FindCollection(collectionName string) (*Collection, bool) {
c, hasCollection := t.collectionMap.Find(collectionName)
if !hasCollection {
return nil, false
}
return c.(*Collection), hasCollection
}
func (t *Topology) DeleteCollection(collectionName string) {
// The layouts vanish with the collection, but every location they served
// holds a bit in its node's lookup digest. Left in place, those bits keep
// the node's held and servable digests apart forever, and the master asks
// for the full volume list on every heartbeat from then on.
// Unpublish first so a racing registration re-resolves into a fresh collection.
collection, found := t.collectionMap.Delete(collectionName)
if !found {
return
}
var vids []needle.VolumeId
for _, vl := range collection.(*Collection).GetAllVolumeLayouts() {
vids = append(vids, vl.ListVids()...)
vl.releaseLookupOwnership()
}
// a vid colliding with a copy in this collection is no longer a duplicate
for _, vid := range vids {
t.releaseResolvedQuarantine(vid)
}
}
func (t *Topology) DeleteLayout(collectionName string, rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType) {
collection, found := t.FindCollection(collectionName)
if !found {
return
}
collection.DeleteVolumeLayout(rp, ttl, diskType)
if len(collection.storageType2VolumeLayout.Items()) == 0 {
t.DeleteCollection(collectionName)
}
}
func (t *Topology) RegisterVolumeLayout(v storage.VolumeInfo, dn *DataNode) {
diskType := types.ToDiskType(v.DiskType)
for {
vl := t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
// RegisterVolume reads the quarantine record and marks under
// accessLock; holding quarantineLock keeps that pair serialized with
// releaseResolvedQuarantine's clear-and-delete.
t.quarantineLock.Lock()
registered := vl.RegisterVolume(&v, dn, func(n *DataNode) bool {
return t.isQuarantined(n, v.Id)
})
t.quarantineLock.Unlock()
if registered {
// RegisterVolume rebuilds only the read-only flags, so a
// location list recreated flagless would pass the writable
// check before the next heartbeat re-measures the volume.
// This report carries the size; mark from it first. A report
// that carries none leaves the remembered verdict in place.
if v.Size > 0 {
vl.UpdateOversizedState(&v, dn)
}
vl.EnsureCorrectWritables(&v)
t.flagDuplicateVolumeIdAsync(v, dn)
return
}
// Dropped with its collection; the next lookup creates a fresh one.
}
}
func (t *Topology) flagDuplicateVolumeIdAsync(v storage.VolumeInfo, dn *DataNode) {
t.duplicateFlagChecks.Add(1)
go func() {
defer t.duplicateFlagChecks.Done()
t.flagDuplicateVolumeId(v, dn)
}()
}
// flagDuplicateVolumeId reports and quarantines v.Id when another data node
// already serves it under a different layout key. It runs after registration
// so a legitimate same-node move (disk type or collection change) settles
// first and does not trip it. Every copy across the conflicting layouts is
// quarantined: either could be the impostor, and arrival order must not
// decide which copy stays writable.
func (t *Topology) flagDuplicateVolumeId(v storage.VolumeInfo, dn *DataNode) {
// Every pass ends with a release: a conflict could resolve between the
// scan below and the marks, and a check finding no hits is also the retry
// a kept record waits for once its replica reports a readable layout.
defer t.releaseResolvedQuarantine(v.Id)
hits := t.findDuplicateVolumeLayouts(v, dn)
if len(hits) == 0 {
return
}
// This runs on a goroutine after registration, so the reporter's copy may
// already be unregistered (a finished move drops the losing copy). The
// surviving copies are then legitimate, and flagging them quarantines a
// healthy volume.
own := t.FindVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, types.ToDiskType(v.DiskType))
if own == nil || !own.HasDataNode(v.Id, dn) {
return
}
stats.MasterDuplicateVolumeIds.WithLabelValues(v.Collection, v.Id.String()).Inc()
var otherKeys []string
for _, h := range hits {
otherKeys = append(otherKeys, h.layoutKey())
}
glog.Errorf("duplicate volume id %d: %s reports it under %s but other data nodes also serve it under %s; marking all copies read-only", v.Id, dn.Id(), volumeLayoutKey(v.Collection, v.ReplicaPlacement, v.Ttl, types.ToDiskType(v.DiskType)), strings.Join(otherKeys, ", "))
quarantine := func(n *DataNode, vl *VolumeLayout) {
q := quarantinedVolume{n.Id(), v.Id}
reported := false
if vi, err := n.GetVolumesById(v.Id); err == nil {
reported = vi.ReadOnly
}
t.quarantineLock.Lock()
t.quarantinedVolumeIds.LoadOrStore(q, quarantinedVolumeState{reported: reported})
if vl != nil {
vl.SetVolumeQuarantined(n, v.Id)
}
t.quarantineLock.Unlock()
if t.QuarantineVolumeFn == nil {
return
}
pushed, err := t.pushQuarantine(n, v.Id, true)
if err != nil {
return
}
t.quarantineLock.Lock()
val, ok := t.quarantinedVolumeIds.Load(q)
if ok {
state := val.(quarantinedVolumeState)
state.enforced = state.enforced || pushed
t.quarantinedVolumeIds.Store(q, state)
t.quarantineLock.Unlock()
return
}
t.quarantineLock.Unlock()
if pushed && !reported {
// The conflict resolved while the push was in flight; leave the
// server consistent with the released layout.
t.releaseQuarantineWritable(n, v.Id)
}
}
quarantine(dn, own)
for _, h := range hits {
for _, n := range h.nodes {
quarantine(n, h.vl)
}
}
}
// releaseResolvedQuarantine lifts read-only marks once the conflicting copy
// is gone, so a finished move does not leave the surviving copy quarantined.
// The writable push is a remote call, so the locked pass only applies the
// layout marks and records which replicas still need it; the pushes run
// after the lock is dropped, a skipped push means a newer record re-armed
// the pair so its quarantine stands, and a failed push re-adds the record
// so a later release pass retries it.
func (t *Topology) releaseResolvedQuarantine(vid needle.VolumeId) {
var writablePushes []*DataNode
t.quarantineLock.Lock()
t.quarantinedVolumeIds.Range(func(key, val any) bool {
q := key.(quarantinedVolume)
if q.vid != vid {
return true
}
dn, vl, vi := t.findQuarantinedVolume(q.dnId, vid)
if dn == nil {
t.quarantinedVolumeIds.Delete(key)
return true
}
if vi.ReplicaPlacement == nil {
// The replica is in the lookup index but unreadable on its node,
// so its layout key is unknown and the conflict cannot be ruled
// out; keep the mark for a later pass.
return true
}
if hits := t.findDuplicateVolumeLayouts(vi, dn); len(hits) > 0 {
return true
}
state, _ := val.(quarantinedVolumeState)
if state.reported {
dn.SetVolumeReadOnly(vid, true)
vl.SetReplicaReadOnlyFlag(dn, vid, true)
} else {
vl.SetVolumeWritable(dn, vid)
if state.enforced && t.QuarantineVolumeFn != nil {
writablePushes = append(writablePushes, dn)
}
}
t.quarantinedVolumeIds.Delete(key)
return true
})
t.quarantineLock.Unlock()
for _, dn := range writablePushes {
t.releaseQuarantineWritable(dn, vid)
}
}
// restoreQuarantineRecord re-adds a released record after a failed writable
// push so the next volume report retries the release. An existing record keeps its
// reported flag; the server is known read-only, so enforced and retry are set.
// A record restored from scratch also gets its layout read-only mark back:
// the release pass cleared it with the record, and leaving it cleared would
// advertise a writable replica the server still holds read-only.
func (t *Topology) restoreQuarantineRecord(dn *DataNode, vid needle.VolumeId) {
t.quarantineLock.Lock()
defer t.quarantineLock.Unlock()
key := quarantinedVolume{dn.Id(), vid}
if cur, ok := t.quarantinedVolumeIds.LoadOrStore(key, quarantinedVolumeState{enforced: true, retry: true}); ok {
state := cur.(quarantinedVolumeState)
state.enforced = true
state.retry = true
t.quarantinedVolumeIds.Store(key, state)
return
}
if _, vl, _ := t.findQuarantinedVolume(dn.Id(), vid); vl != nil {
vl.SetVolumeReadOnly(dn, vid)
}
}
func (t *Topology) findQuarantinedVolume(dnId NodeId, vid needle.VolumeId) (*DataNode, *VolumeLayout, storage.VolumeInfo) {
for _, item := range t.collectionMap.Items() {
for _, vl := range item.(*Collection).GetAllVolumeLayouts() {
for _, n := range vl.Lookup(vid) {
if n.Id() == dnId {
vi, _ := n.GetVolumesById(vid)
return n, vl, vi
}
}
}
}
return nil, nil, storage.VolumeInfo{}
}
// duplicateHit is one layout that holds v.Id on data nodes other than the
// reporter's.
type duplicateHit struct {
collection string
vl *VolumeLayout
nodes []*DataNode
}
func (h duplicateHit) layoutKey() string {
return volumeLayoutKey(h.collection, h.vl.rp, h.vl.ttl, h.vl.diskType)
}
func volumeLayoutKey(collection string, rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType) string {
return fmt.Sprintf("collection=%q rp=%s ttl=%s disk=%s", collection, rp, ttl, diskType)
}
func ttlEqual(a, b *needle.TTL) bool {
if a == nil || b == nil {
return a == b
}
return a.ToUint32() == b.ToUint32()
}
// findDuplicateVolumeLayouts reports every layout that already holds v.Id on
// data nodes other than dn. Volume ids are global, so the same id held by two
// nodes under two layouts means the allocator reused an id.
func (t *Topology) findDuplicateVolumeLayouts(v storage.VolumeInfo, dn *DataNode) []duplicateHit {
if v.ReplicaPlacement == nil {
return nil
}
diskType := types.ToDiskType(v.DiskType)
var hits []duplicateHit
for _, item := range t.collectionMap.Items() {
c := item.(*Collection)
for _, vl := range c.GetAllVolumeLayouts() {
if c.Name == v.Collection && vl.diskType == diskType && ttlEqual(vl.ttl, v.Ttl) {
// The reporter's own layout, or an rp-only difference:
// a replication change rolls through the nodes one at a
// time, so the same volume legitimately reports the old
// and new rp while it settles.
continue
}
var others []*DataNode
for _, n := range vl.Lookup(v.Id) {
if n != dn {
others = append(others, n)
}
}
if len(others) > 0 {
hits = append(hits, duplicateHit{collection: c.Name, vl: vl, nodes: others})
}
}
}
return hits
}
func (t *Topology) UnRegisterVolumeLayout(v storage.VolumeInfo, dn *DataNode) {
glog.Infof("removing volume info: %+v from %v", v, dn.id)
defer t.releaseResolvedQuarantine(v.Id)
diskType := types.ToDiskType(v.DiskType)
volumeLayout := t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
volumeLayout.UnRegisterVolume(&v, dn)
// Drop the series only after the last placement is gone. Deleting while
// another data node still holds v would hide under-replication until the
// next CollectDeadNodeAndFullVolumes cycle recreates the label.
if v.ReplicaPlacement.GetCopyCount() > 1 && len(t.Lookup(v.Collection, v.Id)) == 0 {
stats.MasterReplicaPlacementMismatch.DeleteLabelValues(v.Collection, v.Id.String())
}
if volumeLayout.isEmpty() {
t.DeleteLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
}
}
func (t *Topology) DataCenterExists(dcName string) bool {
return dcName == "" || t.GetDataCenter(dcName) != nil
}
// RackExists reports whether a rack with the id exists under dcName; an
// empty dcName searches every data center, an empty rackName always matches.
func (t *Topology) RackExists(dcName, rackName string) bool {
if rackName == "" {
return true
}
t.RLock()
defer t.RUnlock()
for _, c := range t.children {
if dcName != "" && string(c.Id()) != dcName {
continue
}
for _, r := range c.Children() {
if string(r.Id()) == rackName {
return true
}
}
}
return false
}
// DataNodeExists reports whether a data node with the id exists under
// dcName/rackName; empty parents search every data center and rack, an empty
// nodeId always matches.
func (t *Topology) DataNodeExists(dcName, rackName, nodeId string) bool {
if nodeId == "" {
return true
}
t.RLock()
defer t.RUnlock()
for _, c := range t.children {
if dcName != "" && string(c.Id()) != dcName {
continue
}
for _, r := range c.Children() {
if rackName != "" && string(r.Id()) != rackName {
continue
}
for _, dn := range r.Children() {
if string(dn.Id()) == nodeId {
return true
}
}
}
}
return false
}
func (t *Topology) GetDataCenter(dcName string) (dc *DataCenter) {
t.RLock()
defer t.RUnlock()
for _, c := range t.children {
dc = c.(*DataCenter)
if string(dc.Id()) == dcName {
return dc
}
}
return dc
}
// GrowthBlocker reports why a volume grow under option could never place the
// replica set it needs, mirroring findEmptySlotsForOneVolume's feasibility:
// the option's location binds only the first replica's data center, rack, and
// data node — later replicas land anywhere with room — so counting every
// candidate against the named location condemns growth that can succeed. Nil
// means a grow can still place. Free slots are leaf-summed and ignore
// reservations, so an in-flight grow's own holds still read as room and its
// delivery lands as a writable the assign re-picks.
func (t *Topology) GrowthBlocker(option *VolumeGrowOption) error {
rp := option.ReplicaPlacement
needDCs := rp.DiffDataCenterCount + 1
needRacks := rp.DiffRackCount + 1
needNodes := rp.SameRackCount + 1
t.RLock()
defer t.RUnlock()
roomDCs := 0
mainFits := false
var errs []string
for _, c := range t.children {
var dcFree int64
okRacks := 0
mainRackFits := false
for _, r := range c.Children() {
var rackFree int64
nodesWithRoom := 0
namedNodeFits := option.DataNode == ""
for _, dn := range r.Children() {
free := dn.AvailableSpaceFor(option)
if free <= 0 {
continue
}
nodesWithRoom++
rackFree += free
if string(dn.Id()) == option.DataNode {
namedNodeFits = true
}
}
dcFree += rackFree
if nodesWithRoom >= needNodes {
okRacks++
}
if nodesWithRoom >= needNodes && namedNodeFits &&
(option.Rack == "" || string(r.Id()) == option.Rack) {
mainRackFits = true
}
}
if dcFree > 0 {
roomDCs++
}
if option.DataCenter != "" && string(c.Id()) != option.DataCenter {
continue
}
switch {
case len(c.Children()) < needRacks:
errs = append(errs, fmt.Sprintf("%s: %d racks, needs %d", c.Id(), len(c.Children()), needRacks))
case okRacks < needRacks:
errs = append(errs, fmt.Sprintf("%s: %d racks with %d data nodes with room, needs %d", c.Id(), okRacks, needNodes, needRacks))
case (option.Rack != "" || option.DataNode != "") && !mainRackFits:
errs = append(errs, fmt.Sprintf("%s: no rack fits the named placement", c.Id()))
case dcFree < int64(needRacks-1+needNodes):
errs = append(errs, fmt.Sprintf("%s: %d free volume slots, needs %d", c.Id(), dcFree, needRacks-1+needNodes))
default:
mainFits = true
}
}
if mainFits && roomDCs >= needDCs {
return nil
}
if mainFits {
return fmt.Errorf("%s can grow in %s: %d data centers with room, needs %d", NoWritableVolumes, option, roomDCs, needDCs)
}
if len(errs) == 0 {
return fmt.Errorf("%s: no data center matches %s", NoWritableVolumes, option)
}
return fmt.Errorf("%s can grow in %s: %s", NoWritableVolumes, option, strings.Join(errs, "; "))
}
func (t *Topology) GetOrCreateDataCenter(dcName string) *DataCenter {
t.Lock()
defer t.Unlock()
for _, c := range t.children {
dc := c.(*DataCenter)
if string(dc.Id()) == dcName {
return dc
}
}
dc := NewDataCenter(dcName)
t.doLinkChildNode(dc)
return dc
}
func (t *Topology) ListDataCenters() (dcs []string) {
t.RLock()
defer t.RUnlock()
for _, c := range t.children {
dcs = append(dcs, string(c.(*DataCenter).Id()))
}
return dcs
}
func (t *Topology) ListDCAndRacks() (dcs map[NodeId][]NodeId) {
t.RLock()
defer t.RUnlock()
dcs = make(map[NodeId][]NodeId)
for _, dcNode := range t.children {
dcNodeId := dcNode.(*DataCenter).Id()
for _, rackNode := range dcNode.Children() {
dcs[dcNodeId] = append(dcs[dcNodeId], rackNode.(*Rack).Id())
}
}
return dcs
}
func (t *Topology) SyncDataNodeRegistration(volumes []*master_pb.VolumeInformationMessage, dn *DataNode) (newVolumes, deletedVolumes, changedVolumes []storage.VolumeInfo) {
// convert into in memory struct storage.VolumeInfo
volumeInfos := make([]storage.VolumeInfo, 0, len(volumes))
for _, v := range volumes {
if vi, err := storage.NewVolumeInfo(v); err == nil {
volumeInfos = append(volumeInfos, vi)
} else {
glog.V(0).Infof("Fail to convert joined volume information: %v", err)
}
}
for i := range volumeInfos {
volumeInfos[i] = t.enforceQuarantine(dn, volumeInfos[i])
}
// find out the delta volumes
newVolumes, deletedVolumes, changedVolumes = dn.UpdateVolumes(volumeInfos)
for _, v := range newVolumes {
t.RegisterVolumeLayout(v, dn)
}
for _, v := range deletedVolumes {
t.UnRegisterVolumeLayout(v, dn)
}
for _, v := range changedVolumes {
if v.ReplicaPlacement == nil {
continue
}
t.quarantineLock.Lock()
t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, types.ToDiskType(v.DiskType)).SetReplicaReadOnlyFlag(dn, v.Id, v.ReadOnly || t.isQuarantined(dn, v.Id))
t.quarantineLock.Unlock()
}
// Update effective sizes for all reported volumes (decay pending estimates).
// If decay brings a volume eagerly removed by RecordAssign back under the
// writable threshold, restore the matching activeVolumeCount.
for _, v := range volumeInfos {
if v.ReplicaPlacement == nil {
continue
}
diskType := types.ToDiskType(v.DiskType)
vl := t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
// Self-heal: a volume reported by the data node but missing from the
// lookup index is re-registered. This repairs the split left by a
// disconnect/reconnect race, where UnRegisterDataNode dropped the volume
// from vid2location but the reconnecting full heartbeat skipped it
// (still in the disk map, so UpdateVolumes did not report it as new).
// Without this, the volume stays visible in volume.list/admin UI yet
// LookupVolume returns "volume id not found".
if !vl.HasDataNode(v.Id, dn) {
for {
t.quarantineLock.Lock()
registered := vl.RegisterVolume(&v, dn, func(n *DataNode) bool {
return t.isQuarantined(n, v.Id)
})
t.quarantineLock.Unlock()
if registered {
break
}
// Dropped with its collection; the next lookup creates a fresh
// one, which the calls below must use too.
vl = t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
}
t.flagDuplicateVolumeIdAsync(v, dn)
// Volumes new to the disk map were registered above, so reaching
// here means only the lookup index had lost it. Clients were told
// it went when the node dropped out, so the repair has to tell them
// it is back.
newVolumes = append(newVolumes, v)
}
vl.UpdateOversizedState(&v, dn)
if vl.UpdateVolumeSize(v.Id, v.Size, v.CompactRevision, true) {
vl.AdjustActiveVolumeCountAfterRecovery(v.Id)
}
vl.EnsureCorrectWritables(&v)
}
return
}
func (t *Topology) IncrementalSyncDataNodeRegistration(newVolumes, deletedVolumes []*master_pb.VolumeShortInformationMessage, dn *DataNode) {
var newVis, oldVis []storage.VolumeInfo
for _, v := range newVolumes {
vi, err := storage.NewVolumeInfoFromShort(v)
if err != nil {
glog.V(0).Infof("NewVolumeInfoFromShort %v: %v", v, err)
continue
}
newVis = append(newVis, t.enforceQuarantine(dn, vi))
}
for _, v := range deletedVolumes {
vi, err := storage.NewVolumeInfoFromShort(v)
if err != nil {
glog.V(0).Infof("NewVolumeInfoFromShort %v: %v", v, err)
continue
}
oldVis = append(oldVis, vi)
}
dn.DeltaUpdateVolumes(newVis, oldVis)
type layoutKey struct {
id needle.VolumeId
collection string
replicaPlacement byte
ttl uint32
diskType types.DiskType
}
key := func(vi storage.VolumeInfo) layoutKey {
return layoutKey{
id: vi.Id,
collection: vi.Collection,
replicaPlacement: vi.ReplicaPlacement.Byte(),
ttl: vi.Ttl.ToUint32(),
diskType: types.ToDiskType(vi.DiskType),
}
}
replacements := make(map[layoutKey]struct{}, len(newVis))
for _, vi := range newVis {
replacements[key(vi)] = struct{}{}
t.RegisterVolumeLayout(vi, dn)
}
for _, oldVi := range oldVis {
if _, replaced := replacements[key(oldVi)]; !replaced {
t.UnRegisterVolumeLayout(oldVi, dn)
}
}
return
}
// ApplyVolumeChanges records the volumes a heartbeat reported as changed and
// returns the ones the node did not already have. Only the named volumes are
// touched: unlike a full report, silence about a volume says nothing about
// whether the server still has it.
//
// Most changes are a volume growing, which moves no location, so returning
// only the arrivals keeps a busy cluster from telling every client about
// volumes they can already reach. The arrival set also includes replicas whose
// IsRemote() classification flipped on tier transition: the volume is still
// servable from the same node, but every connected client has stale replica
// priority and must be told to refresh.
func (t *Topology) ApplyVolumeChanges(changed []*master_pb.VolumeInformationMessage, dn *DataNode) (newVolumes []storage.VolumeInfo) {
volumeInfos := make([]storage.VolumeInfo, 0, len(changed))
for _, v := range changed {
vi, err := storage.NewVolumeInfo(v)
if err != nil {
glog.V(0).Infof("Fail to convert changed volume information: %v", err)
continue
}
volumeInfos = append(volumeInfos, t.enforceQuarantine(dn, vi))
}
for _, vi := range volumeInfos {
isNew, isChanged, tierTransition := dn.AddOrUpdateVolume(vi)
if vi.ReplicaPlacement == nil {
if isNew {
newVolumes = append(newVolumes, vi)
}
continue
}
vl := t.GetVolumeLayout(vi.Collection, vi.ReplicaPlacement, vi.Ttl, types.ToDiskType(vi.DiskType))
// Reaching the lookup index is what makes a volume servable, so a
// volume only that index had lost is an arrival as far as clients are
// concerned: they were told it went when the node dropped out.
becameServable := !vl.HasDataNode(vi.Id, dn)
for becameServable {
t.quarantineLock.Lock()
registered := vl.RegisterVolume(&vi, dn, func(n *DataNode) bool {
return t.isQuarantined(n, vi.Id)
})
t.quarantineLock.Unlock()
if registered {
break
}
// Dropped with its collection; the next lookup creates a fresh one.
vl = t.GetVolumeLayout(vi.Collection, vi.ReplicaPlacement, vi.Ttl, types.ToDiskType(vi.DiskType))
}
if becameServable {
t.flagDuplicateVolumeIdAsync(vi, dn)
}
if isNew || becameServable || tierTransition || isChanged {
newVolumes = append(newVolumes, vi)
}
if isChanged {
t.quarantineLock.Lock()
vl.SetReplicaReadOnlyFlag(dn, vi.Id, vi.ReadOnly || t.isQuarantined(dn, vi.Id))
t.quarantineLock.Unlock()
}
vl.UpdateOversizedState(&vi, dn)
if vl.UpdateVolumeSize(vi.Id, vi.Size, vi.CompactRevision, true) {
vl.AdjustActiveVolumeCountAfterRecovery(vi.Id)
}
vl.EnsureCorrectWritables(&vi)
}
return newVolumes
}
func (t *Topology) DataNodeRegistration(dcName, rackName string, dn *DataNode) {
if dn.Parent() != nil {
return
}
// registration to topo
dc := t.GetOrCreateDataCenter(dcName)
rack := dc.GetOrCreateRack(rackName)
rack.LinkChildNode(dn)
glog.Infof("[%s] reLink To topo ", dn.Id())
}
// IsVacuumDisabled returns true if vacuum is disabled by either the
// operator or the plugin monitor.
func (t *Topology) IsVacuumDisabled() bool {
return t.vacuumDisabledByOperator.Load() || t.vacuumDisabledByPlugin.Load()
}
// DisableVacuum is called by the operator (shell command / manual RPC).
// Only sets the operator flag; does not affect the plugin flag.
func (t *Topology) DisableVacuum() {
glog.V(0).Infof("DisableVacuum (by operator)")
t.vacuumDisabledByOperator.Store(true)
}
// EnableVacuum is called by the operator (shell command / manual RPC).
// Only clears the operator flag; does not affect the plugin flag.
func (t *Topology) EnableVacuum() {
glog.V(0).Infof("EnableVacuum (by operator)")
t.vacuumDisabledByOperator.Store(false)
}
// DisableVacuumByPlugin is called by the admin server's vacuum monitor
// when a vacuum plugin worker connects. Only sets the plugin flag.
func (t *Topology) DisableVacuumByPlugin() {
glog.V(0).Infof("DisableVacuum (by plugin worker)")
t.vacuumDisabledByPlugin.Store(true)
}
// EnableVacuumByPlugin is called by the admin server's vacuum monitor
// when a vacuum plugin worker disconnects. Only clears the plugin flag.
func (t *Topology) EnableVacuumByPlugin() {
glog.V(0).Infof("EnableVacuum (by plugin worker)")
t.vacuumDisabledByPlugin.Store(false)
}
// IsVacuumDisabledByPlugin returns whether the plugin monitor has disabled vacuum.
func (t *Topology) IsVacuumDisabledByPlugin() bool {
return t.vacuumDisabledByPlugin.Load()
}
// SetAdminServerConnectedFunc sets an optional callback used by the vacuum
// safety net to detect when the admin server has disconnected.
func (t *Topology) SetAdminServerConnectedFunc(f func() bool) {
t.adminServerConnectedFunc = f
}
func (t *Topology) GetTopologyId() string {
t.topologyIdLock.RLock()
defer t.topologyIdLock.RUnlock()
return t.topologyId
}
func (t *Topology) SetTopologyId(topologyId string) {
t.topologyIdLock.Lock()
defer t.topologyIdLock.Unlock()
if topologyId == "" {
return
}
if t.topologyId == "" {
t.topologyId = topologyId
return
}
if t.topologyId != topologyId {
glog.Fatalf("Split-brain detected! Current TopologyId is %s, but received %s. Stopping to prevent data corruption.", t.topologyId, topologyId)
}
}
func (t *Topology) SetGrowthFreezeConfig(freezeOnMismatch bool, recoveryWait time.Duration) {
t.freezeOnMismatch.Store(freezeOnMismatch)
t.stateLossRecoveryWait.Store(int64(recoveryWait))
}
// CheckReportedTopologyId freezes growth when a volume server reports a
// topology id different from this cluster's: the topology id survives raft
// state cleanup, so a mismatch means the master's state was rebuilt (or a
// foreign disk was cloned in).
func (t *Topology) CheckReportedTopologyId(reported string) {
if reported == "" || !t.freezeOnMismatch.Load() {
return
}
t.reportedTopologyIdsLock.Lock()
freeze := t.checkReportedTopologyIdLocked(reported)
t.reportedTopologyIdsLock.Unlock()
if freeze {
// The decision landed under the lock; the raft wait must not, or a
// slow apply would stall every later report — including one that
// needs to trigger the next freeze.
t.applyGrowthFreeze()
}
}
// checkReportedTopologyIdLocked runs the mismatch check inside
// reportedTopologyIdsLock, the same critical section the init gate drains
// under: a report received before the gate opens is either stashed or frozen
// before the gate can release. It returns whether the freeze must be
// replicated, which the caller does after releasing the lock.
func (t *Topology) checkReportedTopologyIdLocked(reported string) (freeze bool) {
own := t.GetTopologyId()
if own == "" {
// The local topology id is still being minted. Keep the report so a
// later heartbeat echo cannot erase the mismatch on the server side
// before the freeze check ever ran.
if t.pendingReportedTopologyIds == nil {
t.pendingReportedTopologyIds = make(map[string]struct{})
}
t.pendingReportedTopologyIds[reported] = struct{}{}
return false
}
if reported == own {
return false
}
// Only the first mismatch stamps a generation: a re-stamp never reaches
// the log, so bound lifts would target a generation followers never saw.
if t.growthFrozenState.CompareAndSwap(0, time.Now().UnixNano()) {
stats.MasterVolumeGrowthFrozen.Set(1)
glog.Errorf("volume growth frozen: a volume server reports topology id %s but this cluster is %s; the master state was rebuilt. Verify the cluster inventory, then run: volume.growth.resume", reported, own)
return true
}
return false
}
func (t *Topology) IsGrowthFrozen() bool {
frozen, _ := t.GrowthFreezeState()
return frozen
}
// GrowthFreezeState returns the freeze flag and its generation from one
// load, so snapshots persisting both cannot split the pair across a lift.
func (t *Topology) GrowthFreezeState() (frozen bool, frozenAt int64) {
state := t.growthFrozenState.Load()
if state == 0 {
return false, 0
}
if wait := t.stateLossRecoveryWait.Load(); wait > 0 {
// A legacy generation carries no instant; its window counts as
// already elapsed.
if state < 0 || time.Duration(time.Now().UnixNano()-state) > time.Duration(wait) {
if t.growthFrozenState.CompareAndSwap(state, 0) {
stats.MasterVolumeGrowthFrozen.Set(0)
glog.V(0).Infof("volume growth unfrozen after %v", time.Duration(wait))
go t.replicateGrowthUnfreeze(state)
return false, 0
}
state = t.growthFrozenState.Load()
}
}
return state != 0, state
}
// replicateGrowthUnfreeze pushes the recovery-wait lift through raft like
// ResumeVolumeGrowth, so a restart does not replay the freeze. The command
// carries the generation it lifts: a newer freeze — even one committed but
// not yet applied when this node won an election — makes the apply a no-op
// instead of being erased. If the write never lands, the generation is
// re-armed so a later IsGrowthFrozen retries the lift.
func (t *Topology) replicateGrowthUnfreeze(frozenAt int64) {
for attempt := 0; attempt < 3; attempt++ {
if attempt > 0 {
time.Sleep(time.Duration(attempt) * time.Second)
}
// The caller's lift already cleared the flag, so unfrozen is the
// state to expect; anything else armed is a newer generation this
// lift must not touch — and falling through to the re-arm would
// undo a resume that followed it.
if s := t.growthFrozenState.Load(); s != 0 && s != frozenAt {
return
}
if err := t.applyGrowthUnfreeze(frozenAt); err == nil {
return
}
}
if frozenAt != 0 && t.growthFrozenState.CompareAndSwap(0, frozenAt) {
stats.MasterVolumeGrowthFrozen.Set(1)
glog.Warningf("volume growth unfreeze not replicated; freeze re-armed for retry")
}
}
func (t *Topology) ResumeVolumeGrowth() bool {
for {
// The compare binds the resume to the generation it observed: a
// freeze landing between the load and the swap just moves to the
// next iteration, which lifts the generation actually armed.
frozenAt := t.growthFrozenState.Load()
if frozenAt == 0 {
return false
}
if t.growthFrozenState.CompareAndSwap(frozenAt, 0) {
stats.MasterVolumeGrowthFrozen.Set(0)
glog.V(0).Infof("volume growth resumed by operator")
t.applyGrowthUnfreeze(frozenAt)
return true
}
}
}
// SetGrowthFrozen applies a replicated freeze transition on every master. A
// positive frozenAt pins the instant so all nodes share one recovery
// window; a non-positive one — entries written before the field existed —
// becomes the ageless generation, a stable identity a bound lift can still
// match and the recovery check reads as already elapsed. An unfreeze bound
// to unfreezeFor applies only while that exact generation is armed, so a
// stale lift cannot erase a newer freeze; zero lifts unconditionally, for
// entries written before the binding existed.
func (t *Topology) SetGrowthFrozen(frozen bool, frozenAt int64, unfreezeFor int64) {
if frozen {
gen := frozenAt
if gen == 0 {
gen = -1
}
if t.growthFrozenState.Swap(gen) == 0 {
stats.MasterVolumeGrowthFrozen.Set(1)
glog.V(0).Infof("volume growth freeze applied from raft")
}
return
}
if unfreezeFor == 0 {
if t.growthFrozenState.Swap(0) != 0 {
stats.MasterVolumeGrowthFrozen.Set(0)
}
return
}
if t.growthFrozenState.CompareAndSwap(unfreezeFor, 0) {
stats.MasterVolumeGrowthFrozen.Set(0)
}
}
// applyGrowthFreeze replicates the current freeze flag and instant so
// followers keep both across a leader change.
func (t *Topology) applyGrowthFreeze() error {
t.growthFreezeApplyLock.Lock()
defer t.growthFreezeApplyLock.Unlock()
cmd := NewMaxVolumeIdCommand(t.GetMaxVolumeId(), t.GetTopologyId())
frozen := true
cmd.GrowthFrozen = &frozen
cmd.GrowthFrozenAt = t.growthFrozenState.Load()
return t.applyGrowthFreezeCommand(cmd)
}
// applyGrowthUnfreeze replicates a lift bound to the freeze instant the
// caller observed.
func (t *Topology) applyGrowthUnfreeze(frozenAt int64) error {
cmd := NewMaxVolumeIdCommand(t.GetMaxVolumeId(), t.GetTopologyId())
frozen := false
cmd.GrowthFrozen = &frozen
cmd.GrowthUnfreezeFor = frozenAt
return t.applyGrowthFreezeCommand(cmd)
}
// applyGrowthFreezeCommand sends the transition through raft. With no raft
// server (single-master test topologies) it applies locally.
func (t *Topology) applyGrowthFreezeCommand(cmd *MaxVolumeIdCommand) error {
frozen := cmd.GrowthFrozen != nil && *cmd.GrowthFrozen
t.RaftServerAccessLock.RLock()
defer t.RaftServerAccessLock.RUnlock()
if t.RaftServer != nil {
if _, err := t.RaftServer.Do(cmd); err != nil {
glog.Warningf("apply growth freeze %v: %v", frozen, err)
return err
}
return nil
}
if t.HashicorpRaft != nil {
b, err := json.Marshal(cmd)
if err != nil {
glog.Warningf("marshal growth freeze %v: %v", frozen, err)
return err
}
if future := t.HashicorpRaft.Apply(b, time.Second); future.Error() != nil {
glog.Warningf("apply growth freeze %v: %v", frozen, future.Error())
return future.Error()
}
return nil
}
t.SetGrowthFrozen(*cmd.GrowthFrozen, cmd.GrowthFrozenAt, cmd.GrowthUnfreezeFor)
return nil
}
// MarkFreshMint records that this boot minted the TopologyId rather than
// recovering a persisted one — the strongest in-process proof that the
// control plane holds no prior cluster state.
func (t *Topology) MarkFreshMint() {
t.freshMinted.Store(true)
}
// FreshMinted reports whether the current TopologyId was minted this boot.
func (t *Topology) FreshMinted() bool {
return t.freshMinted.Load()
}
// SetTopologyInitPending gates allocation while a topology id is being
// minted, so growth cannot slip in before the warmup window is installed.
// Stashed reports are applied while the gate is still closed: clearing it
// first would let an allocation through before a queued mismatch freezes
// growth.
func (t *Topology) SetTopologyInitPending(pending bool) {
if pending {
t.topologyInitPending.Store(true)
return
}
// The stash is drained and the gate released inside one critical section:
// a mismatching report checked here, or still in flight under the same
// lock, lands its freeze before any allocation can observe the open gate.
t.reportedTopologyIdsLock.Lock()
reported := t.pendingReportedTopologyIds
t.pendingReportedTopologyIds = nil
freeze := false
for id := range reported {
freeze = t.checkReportedTopologyIdLocked(id) || freeze
}
t.topologyInitPending.Store(false)
t.reportedTopologyIdsLock.Unlock()
if freeze {
t.applyGrowthFreeze()
}
}
// ReleaseAllocationOnFirstReport opts the topology into lifting the post-mint
// allocation hold once one full volume-server inventory has been applied, and
// pins the admitted volume-server reporter: the early release is only sound
// when a single embedded server (weed mini) can carry prior cluster state, so
// every other reporter must be refused rather than silently trusted.
func (t *Topology) ReleaseAllocationOnFirstReport(onlyReporter ...string) {
if len(onlyReporter) > 0 {
t.singleReporter.Store(onlyReporter[0])
t.expectedReporter.Store(onlyReporter[0])
}
t.releaseAllocOnReport.Store(true)
}
// ReleaseAllocationOnReport lifts the post-mint allocation hold once the named
// reporter's first clean full inventory has been applied. Unlike
// ReleaseAllocationOnFirstReport it does not refuse other reporters: a
// single-master weed server still admits outside volume servers, so the
// release collapses the hold to a confirm pulse rather than clearing it —
// outside reporters keep a heartbeat interval to land prior-state evidence
// or a freeze before new ids issue.
func (t *Topology) ReleaseAllocationOnReport(expected string) {
t.expectedReporter.Store(expected)
t.releaseAllocOnReport.Store(true)
}
// AdmitVolumeReporter enforces the single-reporter restriction the outright
// release relies on: a late-arriving foreign server would carry cluster state
// the dropped hold was meant to wait for.
func (t *Topology) AdmitVolumeReporter(addr string) bool {
if !t.releaseAllocOnReport.Load() {
return true
}
expected, _ := t.singleReporter.Load().(string)
return expected == "" || expected == addr
}
// NoteInventoryApplied marks a heartbeat's full volume list as applied to this
// topology. Under releaseAllocOnReport it also lifts the post-mint allocation
// hold once the report comes from the expected reporter: the bound raise and
// topology-id freeze checks run while the heartbeat is processed, so nothing
// protected remains pending afterwards.
func (t *Topology) NoteInventoryApplied(reporter string) {
if expected, _ := t.expectedReporter.Load().(string); expected != "" && expected != reporter {
return
}
t.inventoryApplied.Store(true)
if t.releaseAllocOnReport.Load() {
t.releaseAllocationHold()
}
}
// releaseAllocationHold ends a post-mint allocation hold on the expected
// reporter's clean inventory. A pinned single reporter is the only reporter
// admitted, so its inventory settles prior cluster state outright and the
// hold can drop. With outside reporters still admitted the release collapses
// the hold to one heartbeat interval instead: the expected report cannot
// prove no other reporter carries prior cluster state, so they keep a pulse
// to land their evidence or a freeze before new ids issue.
func (t *Topology) releaseAllocationHold() {
if pinned, _ := t.singleReporter.Load().(string); pinned != "" {
t.allocationNotBefore.Store(0)
return
}
confirm := time.Now().Add(time.Duration(t.pulse) * time.Second).UnixNano()
for {
cur := t.allocationNotBefore.Load()
if cur <= confirm {
return
}
if t.allocationNotBefore.CompareAndSwap(cur, confirm) {
return
}
}
}
// DeferAllocation holds off new id allocation for d. Called when the leader
// mints a fresh topology id: on a rebuilt master that window gives veteran
// servers a pulse to raise the bound or trip the freeze.
func (t *Topology) DeferAllocation(d time.Duration) {
// Under release-on-report a full hold is pointless once inventory has
// landed: the veteran heartbeat that mattered has already been applied.
// Outside reporters stay admitted without a pinned single reporter, so
// the hold caps at one confirm pulse rather than disappearing.
if t.releaseAllocOnReport.Load() && t.inventoryApplied.Load() {
if pinned, _ := t.singleReporter.Load().(string); pinned != "" {
return
}
if pulse := time.Duration(t.pulse) * time.Second; d > pulse {
d = pulse
}
}
now := time.Now()
t.allocationNotBefore.Store(now.Add(d).UnixNano())
t.allocationEvidenceSince.Store(now.UnixNano())
// Close the race with a report applied between the check above and the
// store, which would otherwise leave the full window armed.
if t.releaseAllocOnReport.Load() && t.inventoryApplied.Load() {
t.releaseAllocationHold()
}
}
// NoteAllocationEvidence records a heartbeat signal that could still carry
// prior cluster state — a server joining or reporting held volumes, a raised
// id bound, or a foreign topology id. The deferral lifts two pulses after the
// last such signal, so a cluster with nothing left to report stops waiting.
func (t *Topology) NoteAllocationEvidence() {
t.allocationEvidenceSince.Store(time.Now().UnixNano())
}
// SetAllocationDeferral overrides the post-mint allocation hold: zero disables
// it, a positive duration sets the cap, negative restores the warmup window.
func (t *Topology) SetAllocationDeferral(d time.Duration) {
t.allocationDeferral.Store(int64(d))
}
// AllocationDeferralDuration returns the configured post-mint hold, or one
// warmup window when unset.
func (t *Topology) AllocationDeferralDuration() time.Duration {
if d := t.allocationDeferral.Load(); d >= 0 {
return time.Duration(d)
}
return t.WarmupDuration()
}
func (t *Topology) allocationDelay() time.Duration {
deadline := t.allocationNotBefore.Load()
if deadline == 0 {
return 0
}
if quietUntil := t.allocationEvidenceSince.Load() + 2*int64(t.pulse)*int64(time.Second); quietUntil < deadline {
deadline = quietUntil
}
return time.Until(time.Unix(0, deadline))
}
// SetMaxVolumeIdFile mirrors the allocation bound to a file outside raft, so
// a master whose raft state is gone still restores a safe boundary. An
// existing file seeds both the bound and the topology id.
func (t *Topology) SetMaxVolumeIdFile(file string) {
t.maxVolumeIdFile = file
data, err := os.ReadFile(file)
if err != nil {
return
}
cmd := &MaxVolumeIdCommand{}
if err := json.Unmarshal(data, cmd); err != nil {
glog.Warningf("corrupt max volume id file %s: %v", file, err)
return
}
t.lastPersistedMaxVolumeId.Store(uint32(cmd.MaxVolumeId))
t.lastPersistedTopologyId.Store(cmd.TopologyId)
t.UpAdjustMaxVolumeId(cmd.MaxVolumeId)
if cmd.TopologyId != "" {
t.SetTopologyId(cmd.TopologyId)
}
if cmd.GrowthFrozen != nil {
t.lastPersistedGrowthFrozen.Store(*cmd.GrowthFrozen)
t.SetGrowthFrozen(*cmd.GrowthFrozen, cmd.GrowthFrozenAt, 0)
}
}
// PersistMaxVolumeId writes the bound to the mirror file. Called by the
// leader before a raft apply and by both raft implementations on apply and
// recovery, so follower mirrors stay current too.
func (t *Topology) PersistMaxVolumeId(vid needle.VolumeId) error {
topoId := t.GetTopologyId()
frozen, frozenAt := t.GrowthFreezeState()
mirrored := func() bool {
return t.lastPersistedMaxVolumeId.Load() >= uint32(vid) && t.lastPersistedTopologyId.Load() == topoId && t.lastPersistedGrowthFrozen.Load() == frozen
}
if mirrored() {
return nil
}
t.maxVolumeIdFileLock.Lock()
defer t.maxVolumeIdFileLock.Unlock()
if mirrored() {
return nil
}
if t.maxVolumeIdFile == "" {
t.lastPersistedMaxVolumeId.Store(uint32(vid))
t.lastPersistedTopologyId.Store(topoId)
t.lastPersistedGrowthFrozen.Store(frozen)
return nil
}
cmd := NewMaxVolumeIdCommand(vid, topoId)
cmd.GrowthFrozen = &frozen
if frozen {
cmd.GrowthFrozenAt = frozenAt
}
b, err := json.Marshal(cmd)
if err != nil {
return err
}
tmp := t.maxVolumeIdFile + ".tmp"
f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
if _, err := f.Write(b); err != nil {
f.Close()
return err
}
if err := f.Sync(); err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Rename(tmp, t.maxVolumeIdFile); err != nil {
return err
}
if err := util.FsyncDir(filepath.Dir(t.maxVolumeIdFile)); err != nil {
return err
}
t.lastPersistedMaxVolumeId.Store(uint32(vid))
t.lastPersistedTopologyId.Store(topoId)
t.lastPersistedGrowthFrozen.Store(frozen)
return nil
}