From d39b1a12e06cea00ff6ba6f44ba1cf515a3067e7 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 19 Oct 2026 04:11:07 -0700 Subject: [PATCH] master: stash reported topology ids until the local id is minted (#11763) --- weed/topology/growth_freeze_test.go | 18 ++++++++++++++++++ weed/topology/topology.go | 27 ++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/weed/topology/growth_freeze_test.go b/weed/topology/growth_freeze_test.go index 2f35d4a6ac..3b296b14df 100644 --- a/weed/topology/growth_freeze_test.go +++ b/weed/topology/growth_freeze_test.go @@ -66,6 +66,24 @@ func TestCheckReportedTopologyIdBeforeTopologyIdKnown(t *testing.T) { if topo.IsGrowthFrozen() { t.Fatal("empty local topology id should not trigger the freeze") } + + topo.SetTopologyId("cluster-c") + topo.SetTopologyInitPending(false) + if !topo.IsGrowthFrozen() { + t.Fatal("a report stashed before the topology id was known should still freeze growth") + } +} + +func TestReportedTopologyIdStashDoesNotFreezeOnMatch(t *testing.T) { + topo := NewTopology("topo", nil, 128, 3, false) + topo.SetGrowthFreezeConfig(true, 0) + + topo.CheckReportedTopologyId("cluster-a") + topo.SetTopologyId("cluster-a") + topo.SetTopologyInitPending(false) + if topo.IsGrowthFrozen() { + t.Fatal("a stashed report matching the minted id should not freeze growth") + } } func TestGrowthFreezeSurvivesMirrorRestore(t *testing.T) { diff --git a/weed/topology/topology.go b/weed/topology/topology.go index 770ed81821..0c7c421fbf 100644 --- a/weed/topology/topology.go +++ b/weed/topology/topology.go @@ -95,6 +95,9 @@ type Topology struct { stateLossRecoveryWait atomic.Int64 allocationNotBefore atomic.Int64 topologyInitPending atomic.Bool + + reportedTopologyIdsLock sync.Mutex + pendingReportedTopologyIds map[string]struct{} } func NewTopology(id string, seq sequence.Sequencer, volumeSizeLimit uint64, pulse int, replicationAsMin bool) *Topology { @@ -987,7 +990,19 @@ func (t *Topology) CheckReportedTopologyId(reported string) { return } own := t.GetTopologyId() - if own == "" || reported == own { + 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. + t.reportedTopologyIdsLock.Lock() + if t.pendingReportedTopologyIds == nil { + t.pendingReportedTopologyIds = make(map[string]struct{}) + } + t.pendingReportedTopologyIds[reported] = struct{}{} + t.reportedTopologyIdsLock.Unlock() + return + } + if reported == own { return } t.growthFrozenAt.Store(time.Now().UnixNano()) @@ -1065,6 +1080,16 @@ func (t *Topology) applyGrowthFreeze(frozen bool) { // minted, so growth cannot slip in before the warmup window is installed. func (t *Topology) SetTopologyInitPending(pending bool) { t.topologyInitPending.Store(pending) + if pending { + return + } + t.reportedTopologyIdsLock.Lock() + reported := t.pendingReportedTopologyIds + t.pendingReportedTopologyIds = nil + t.reportedTopologyIdsLock.Unlock() + for id := range reported { + t.CheckReportedTopologyId(id) + } } // DeferAllocation holds off new id allocation for d. Called when the leader