Do not process intermediate one if new ones are fresher just use the freshest

This commit is contained in:
riccardom
2026-06-28 17:20:00 +02:00
parent 297dcb3e24
commit 0bf964dad7
2 changed files with 75 additions and 38 deletions
+28 -29
View File
@@ -17,33 +17,29 @@ import (
// desired state. A single background goroutine (run) applies it to the engine in
// bounded passes via apply() until converged, releasing syncMsgMux between passes
// so other subsystems interleave. If a newer update arrives mid-flight, the loop
// coalesces: it keeps converging toward the latest target rather than replaying
// the intermediate ones.
// coalesces: it keeps converging toward the latest target and the intermediate one
// is SKIPPED — never applied on its own (logged, no onConverged).
//
// Convergence is a single comparison: appliedGen == targetGen. targetGen
// increments on every SetTarget (an internal generation counter, so it also covers
// config-only updates that carry no network-map serial).
//
// onConverged still fires once per MAP, not once per convergence: every update's
// receive time is recorded in `pending` and the whole list is drained on each
// settle. So a map that was superseded mid-flight (its apply coalesced into a newer
// target) still gets its finish-sync signal when the client reaches a converged
// state covering it — the signal is delayed at worst, never lost. That log/metric
// is a strong problem indicator and must not silently disappear.
// onConverged fires once for each — and only each — map that is actually processed
// (i.e. converged as the target). Skipped/superseded maps and dropped-on-error maps
// do NOT fire it. So "sync finished in X" / RecordSyncDuration always corresponds
// to a real, completed alignment.
type mapStateManager struct {
// apply performs one bounded apply pass and reports whether more passes are needed.
apply func(*mgmProto.SyncResponse) (bool, error)
// onConverged is called once per applied map, with the elapsed time since that
// onConverged is called once per processed map, with the elapsed time since that
// map was received (for the sync-duration metric / "sync finished" log).
onConverged func(time.Duration)
mu sync.Mutex
target *mgmProto.SyncResponse
targetGen uint64
appliedGen uint64
// pending holds the receive time of every update accepted since the last settle,
// oldest first. Drained on convergence so onConverged fires once per map.
pending []time.Time
mu sync.Mutex
target *mgmProto.SyncResponse
targetGen uint64
appliedGen uint64
targetSetAt time.Time
wake chan struct{}
}
@@ -61,6 +57,12 @@ func newMapStateManager(apply func(*mgmProto.SyncResponse) (bool, error), onConv
// staleness of the network map is still enforced inside apply (updateNetworkMap).
func (m *mapStateManager) SetTarget(update *mgmProto.SyncResponse) error {
m.mu.Lock()
// A target that has not settled yet (targetGen > appliedGen) is being superseded
// before it converged: we coalesce to the latest map and never apply this one on
// its own. It is SKIPPED — logged here, and it will not fire onConverged.
if m.target != nil && m.targetGen > m.appliedGen {
log.Debugf("sync map (gen %d) superseded before convergence, skipping", m.targetGen)
}
m.target = update
// Bump an internal generation counter, NOT the map serial: config-only updates
// (relay token rotation, STUN/TURN) arrive with NetworkMap == nil and carry no
@@ -68,7 +70,7 @@ func (m *mapStateManager) SetTarget(update *mgmProto.SyncResponse) error {
// target regardless of payload. Map-serial staleness is enforced separately
// inside apply (updateNetworkMap).
m.targetGen++
m.pending = append(m.pending, time.Now())
m.targetSetAt = time.Now()
m.mu.Unlock()
select {
@@ -119,21 +121,21 @@ func (m *mapStateManager) run(ctx context.Context) {
continue
}
// This pass converged. Mark applied + signal every pending map.
// This pass converged. Mark applied and signal this one map.
m.settle(tg, true)
// if a newer target arrived mid-pass, settle is a no-op (targetGen != tg) and
// ag<tg next iteration -> apply it; pending carries over until the next settle.
// ag<tg next iteration -> apply it; this generation was skipped (logged in
// SetTarget) and is not signaled.
}
}
// settle marks generation tg as processed so the loop goes idle instead of
// re-applying the same target. It is a no-op when a newer target arrived during the
// pass (targetGen != tg), leaving appliedGen + pending behind so that target
// re-applies and its maps are signaled at the next settle.
// pass (targetGen != tg), leaving appliedGen behind so that target re-applies — the
// just-finished generation was already counted as skipped.
//
// When signal is true (the pass converged) it drains pending and fires onConverged
// once per map. When false (the target was dropped on error) it discards pending
// without signaling — those maps did not converge; management re-delivers them.
// When signal is true (the pass converged) it fires onConverged once for this map;
// when false (the target was dropped on error) it does not — the map did not converge.
func (m *mapStateManager) settle(tg uint64, signal bool) {
m.mu.Lock()
if m.targetGen != tg {
@@ -141,13 +143,10 @@ func (m *mapStateManager) settle(tg uint64, signal bool) {
return
}
m.appliedGen = tg
toSignal := m.pending
m.pending = nil
setAt := m.targetSetAt
m.mu.Unlock()
if signal && m.onConverged != nil {
for _, setAt := range toSignal {
m.onConverged(time.Since(setAt))
}
m.onConverged(time.Since(setAt))
}
}