fix(nodemeta): sync template locality to localcache on periodic reload#730
fix(nodemeta): sync template locality to localcache on periodic reload#730dushulin wants to merge 1 commit into
Conversation
| syncSnaps = append(syncSnaps, cloneSnapshot(newSnap)) | ||
| } | ||
| } | ||
| s.mu.Unlock() |
There was a problem hiding this comment.
Panic safety: Replacing defer s.mu.Unlock() with a bare s.mu.Unlock() here means any panic in the merge loop (lines 865–903) leaves s.mu locked forever. While loopReload catches panics via recov.WithRecover, a leaked mutex causes the next heartbeat handler or label update to deadlock silently — harder to diagnose than a crash.
Suggestion: Extract the critical section (loop body) into a helper with defer s.mu.Unlock(), or restore defer before the function body:
func (s *service) applyReloadResult(next map[string]*NodeSnapshot) {
syncSnaps := s.mergeReloadResult(next) // contains s.mu with defer
if localcache.Ready() {
for _, snap := range syncSnaps {
syncLocalcache(snap)
}
}
}| // | ||
| // Guard on localcache.Ready(): nodemeta.Init() runs an initial reload | ||
| // BEFORE localcache.Init() in coreInit, so the very first reload must skip | ||
| // this (localcache.Init loads all nodes/templates from the DB itself). The |
There was a problem hiding this comment.
Doc inaccuracy: localcache.Init() calls loadAllFromDB() which only populates the scheduler node cache (l.cache). Template locality (imageCache) is NOT loaded by localcache.Init — it is populated later by syncLocalcache calls from nodemeta's register/heartbeat/reload paths. The safety of skipping the sync here is still correct (template locality converges on the next periodic reload or incoming heartbeat), but the stated reason is slightly wrong. Suggest changing "nodes/templates" to "nodes".
| existing.ReportedReady = newSnap.ReportedReady | ||
| } | ||
| applyCurrentHealth(existing, time.Now()) | ||
| syncSnaps = append(syncSnaps, cloneSnapshot(existing)) |
There was a problem hiding this comment.
cloneSnapshot is now called in this new code path. It uses append([]ContainerImage(nil), in.Images...) which creates a new outer slice, but each element's Names []string field still aliases the source backing array. If any future code path mutates Images[i].Names outside s.mu, it would race with the original inside s.nodes. Currently harmless (neither toSchedulerNode nor syncLocalcache accesses Images), but worth noting for future refactors.
Review of PR #730 — fix(nodemeta): sync template locality to localcache on periodic reloadOverallThis is a well-structured fix for a cross-replica template-locality consistency bug. The approach is correct: applyReloadResult now mirrors the register/heartbeat paths by re-syncing each reloaded node into localcache outside the s.mu lock. The split into mergeReloadResult (under lock) + localcache sync (outside lock) is clean. Notable findings
|
00fec3d to
39cbcdf
Compare
| @@ -886,9 +894,27 @@ func (s *service) applyReloadResult(next map[string]*NodeSnapshot) { | |||
| existing.ReportedReady = newSnap.ReportedReady | |||
| } | |||
| applyCurrentHealth(existing, time.Now()) | |||
| syncSnaps = append(syncSnaps, cloneSnapshot(existing)) | |||
| } else { | |||
| // New node discovered from DB (registered on another replica). | |||
| s.nodes[nodeID] = newSnap | |||
| syncSnaps = append(syncSnaps, cloneSnapshot(newSnap)) | |||
| } | |||
| } | |||
| s.mu.Unlock() | |||
There was a problem hiding this comment.
Panic-Safety Concern: Removing defer s.mu.Unlock() in favor of a manual s.mu.Unlock() at line 904 means a panic in the locked section (lines 865-903) will permanently deadlock the mutex, blocking all writers on s.nodes (including the full registration/heartbeat path).
Consider wrapping the locked section in an IIFE to preserve defer's panic safety while keeping the unlock-before-localcache semantics:
syncSnaps := func() []*NodeSnapshot {
s.mu.Lock()
defer s.mu.Unlock()
// ... existing loop ...
return syncSnaps
}()| func InitInMemoryForTest() { | ||
| l.cache = cache.New(0, 0) | ||
| l.imageCache = cache.New(0, 0) | ||
| l.templateNodeCache = cache.New(0, 0) | ||
| l.sortedNodesByClusters = make(map[string]node.NodeList) | ||
| l.sortedNodesByClusters[constants.DefaultInstanceTypeName] = node.NodeList{} | ||
| } |
There was a problem hiding this comment.
Production-Safety Concern: This function is exported with no compile-time guard. A future refactor importing localcache could accidentally call it in production, wiping all node and template-locality state on a live replica.
Consider either:
- Unexporting to
initInMemoryForTest()+ use//go:build testbuild constraint, or - Moving to a
*_test.gofile or atestutilinternal sub-package excluded from production builds.
| func Ready() bool { | ||
| return l.cache != nil | ||
| } |
There was a problem hiding this comment.
Data Race Concern: Ready() reads l.cache without any memory barrier (no mutex, no atomic load), while l.cache is written in Init() and InitInMemoryForTest(). Under the Go memory model, concurrent unsynchronized reads/writes to the same pointer constitute a data race.
Consider using atomic.Bool instead:
var cacheInitialized atomic.Bool
func Ready() bool { return cacheInitialized.Load() }(Set cacheInitialized.Store(true) at the end of Init and InitInMemoryForTest.)
The periodic node-metadata reload (loopReload/applyReloadResult, added in TencentCloud#542) rebuilt only the nodemeta in-memory node map. It did NOT rebuild localcache (scheduler node cache + template locality / imageCache), which is populated solely from the register/heartbeat paths via syncLocalcache. In a multi-replica cubemaster deployment a cubelet typically reports to a single replica (e.g. a LoadBalancer VIP pins each cubelet to one pod), so only that replica's imageCache learned which templates are ready on which node. Any other replica returned GetImageStateByNode == nil in EnsureTemplateLocalityReady and sandbox creation there failed with "template has no ready replica" (error 130400). Creation success was roughly 50/50 depending on which replica the request hit. applyReloadResult now re-syncs every reloaded node into localcache (UpsertNode + SyncNodeTemplates) after releasing s.mu, mirroring the register/heartbeat paths. A replica that only learned the node via the DB reload now also registers its ready template replicas into the imageCache, so template locality is consistent across replicas. The sync is gated on localcache.Ready(): nodemeta.Init() runs an initial reload BEFORE localcache.Init() in coreInit, so the first reload must skip the sync (localcache.Init loads all nodes/templates from the DB itself right after); otherwise it panics on a nil node cache. The periodic loopReload runs long after both packages are initialised. Adds localcache.Ready(), localcache.InitInMemoryForTest (in-memory-only init for cross-package tests) and TestApplyReloadResultSyncsTemplateLocalityToLocalcache. Verified on a 2-replica staging deployment: a 20x sandbox-create loop that was previously ~50% failing (130400) now succeeds 20/20. Signed-off-by: dushulin <986525775@qq.com>
39cbcdf to
cafa8d2
Compare
|
Thanks for your PR! I'll review and test this next Monday~ |
Summary
In a multi-replica CubeMaster deployment, template locality is not synced across replicas on the periodic DB reload, so sandbox creation intermittently fails with
template has no ready replica(error 130400) depending on which replica serves the request.Component
CubeMaster (
pkg/nodemeta+pkg/localcache)Environment
masterSteps to Reproduce
Expected Behavior
Every CubeMaster replica knows which templates are ready on which node, so sandbox creation succeeds regardless of which replica serves the request.
Actual Behavior
Only the replica that received the node's direct register/heartbeat has the template locality in its
imageCache. Any other replica returnsGetImageStateByNode == nilinEnsureTemplateLocalityReady, and creation fails withtemplate has no ready replica(error 130400). In a 2-replica setup, creation succeeded roughly 50/50 depending on which replica the request hit.Root Cause
The periodic node-metadata reload (
loopReload/applyReloadResult, added in #542 for cross-replica node/heartbeat sync) rebuilds only thenodemetain-memory node map. It does not rebuildlocalcache(scheduler node cache + template locality /imageCache), which is populated solely from the register/heartbeat paths viasyncLocalcache. So a replica that learns a node only via the DB reload never registers that node's ready template replicas into itsimageCache.This is the template-locality analogue of the heartbeat bug fixed in #542: node registration/heartbeat is now synced across replicas, but template locality was still per-process.
Fix
applyReloadResultnow re-syncs every reloaded node intolocalcache(UpsertNode+SyncNodeTemplates) after releasings.mu, mirroring the register/heartbeat paths. A replica that only learned the node via the DB reload now also registers its ready template replicas into theimageCache, so template locality converges across replicas within oneSyncMetaDataInterval.The sync is gated on a new
localcache.Ready()check:nodemeta.Init()runs an initial reload beforelocalcache.Init()incoreInit, so the first reload must skip the sync (localcache.Initloads all nodes/templates from the DB itself right after); otherwise it would panic on a nil node cache. The periodicloopReloadruns long after both packages are initialised.Snapshots are cloned while holding
s.muandsyncLocalcacheis called after unlocking, to avoid holding the nodemeta lock across localcache calls.Testing
TestApplyReloadResultSyncsTemplateLocalityToLocalcache: models a replica with no in-memory node that learns the node (and its local templates) only via DB reload, and assertsGetImageStateByNodeis non-nil afterapplyReloadResult.localcache.InitInMemoryForTest(in-memory-only init) so cross-packagenodemetatests can exercise the sync path without a fullInit(DB / Redis / goroutines).go test ./CubeMaster/pkg/nodemeta/...passes.healthy=true.Related
fix(nodemeta): start periodic reload goroutine for cross-replica node sync), which fixed the same cross-replica gap for node heartbeat/health but not for template locality.