mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-23 22:09:09 +02:00
Always run onConverged for every map that is processed
This commit is contained in:
+48
-28
@@ -17,23 +17,33 @@ import (
|
|||||||
// desired state. A single background goroutine (run) applies it to the engine in
|
// desired state. A single background goroutine (run) applies it to the engine in
|
||||||
// bounded passes via apply() until converged, releasing syncMsgMux between passes
|
// bounded passes via apply() until converged, releasing syncMsgMux between passes
|
||||||
// so other subsystems interleave. If a newer update arrives mid-flight, the loop
|
// so other subsystems interleave. If a newer update arrives mid-flight, the loop
|
||||||
// keeps converging toward the latest target.
|
// coalesces: it keeps converging toward the latest target rather than replaying
|
||||||
|
// the intermediate ones.
|
||||||
//
|
//
|
||||||
// State is a single comparison: appliedGen == targetGen means converged.
|
// Convergence is a single comparison: appliedGen == targetGen. targetGen
|
||||||
// targetGen increments on every SetTarget (an internal generation counter, so it
|
// increments on every SetTarget (an internal generation counter, so it also covers
|
||||||
// also covers config-only updates that carry no network-map serial).
|
// 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.
|
||||||
type mapStateManager struct {
|
type mapStateManager struct {
|
||||||
// apply performs one bounded apply pass and reports whether more passes are needed.
|
// apply performs one bounded apply pass and reports whether more passes are needed.
|
||||||
apply func(*mgmProto.SyncResponse) (bool, error)
|
apply func(*mgmProto.SyncResponse) (bool, error)
|
||||||
// onConverged is called once per target when it is fully applied, with the
|
// onConverged is called once per applied map, with the elapsed time since that
|
||||||
// elapsed time since that target was set (for the sync-duration metric).
|
// map was received (for the sync-duration metric / "sync finished" log).
|
||||||
onConverged func(time.Duration)
|
onConverged func(time.Duration)
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
target *mgmProto.SyncResponse
|
target *mgmProto.SyncResponse
|
||||||
targetGen uint64
|
targetGen uint64
|
||||||
appliedGen uint64
|
appliedGen uint64
|
||||||
targetSetAt time.Time
|
// 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
|
||||||
|
|
||||||
wake chan struct{}
|
wake chan struct{}
|
||||||
}
|
}
|
||||||
@@ -58,7 +68,7 @@ func (m *mapStateManager) SetTarget(update *mgmProto.SyncResponse) error {
|
|||||||
// target regardless of payload. Map-serial staleness is enforced separately
|
// target regardless of payload. Map-serial staleness is enforced separately
|
||||||
// inside apply (updateNetworkMap).
|
// inside apply (updateNetworkMap).
|
||||||
m.targetGen++
|
m.targetGen++
|
||||||
m.targetSetAt = time.Now()
|
m.pending = append(m.pending, time.Now())
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -72,7 +82,7 @@ func (m *mapStateManager) SetTarget(update *mgmProto.SyncResponse) error {
|
|||||||
func (m *mapStateManager) run(ctx context.Context) {
|
func (m *mapStateManager) run(ctx context.Context) {
|
||||||
for {
|
for {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
target, tg, ag, setAt := m.target, m.targetGen, m.appliedGen, m.targetSetAt
|
target, tg, ag := m.target, m.targetGen, m.appliedGen
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
// Fully converged (or nothing yet): block until a new target arrives.
|
// Fully converged (or nothing yet): block until a new target arrives.
|
||||||
@@ -99,7 +109,7 @@ func (m *mapStateManager) run(ctx context.Context) {
|
|||||||
// where the apply error was logged by the gRPC client and the update
|
// where the apply error was logged by the gRPC client and the update
|
||||||
// dropped. No onConverged: this target did not converge.
|
// dropped. No onConverged: this target did not converge.
|
||||||
log.Errorf("apply sync pass, dropping update: %v", err)
|
log.Errorf("apply sync pass, dropping update: %v", err)
|
||||||
m.markProcessed(tg)
|
m.settle(tg, false)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,25 +119,35 @@ func (m *mapStateManager) run(ctx context.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// This pass converged. Mark applied only if no newer target arrived during it.
|
// This pass converged. Mark applied + signal every pending map.
|
||||||
if m.markProcessed(tg) && m.onConverged != nil {
|
m.settle(tg, true)
|
||||||
m.onConverged(time.Since(setAt))
|
// 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.
|
||||||
// if a newer target arrived mid-pass, ag<tg next iteration -> apply it
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// markProcessed records that the loop has finished working generation tg (whether
|
// settle marks generation tg as processed so the loop goes idle instead of
|
||||||
// it converged or the pass was dropped on error), so it goes idle instead of
|
// re-applying the same target. It is a no-op when a newer target arrived during the
|
||||||
// re-applying the same target. It is a no-op when a newer target arrived during
|
// pass (targetGen != tg), leaving appliedGen + pending behind so that target
|
||||||
// the pass (targetGen != tg), leaving appliedGen behind so that target re-applies.
|
// re-applies and its maps are signaled at the next settle.
|
||||||
// Returns true when tg was the latest target (i.e. this was a genuine settle).
|
//
|
||||||
func (m *mapStateManager) markProcessed(tg uint64) bool {
|
// 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.
|
||||||
|
func (m *mapStateManager) settle(tg uint64, signal bool) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
|
||||||
if m.targetGen != tg {
|
if m.targetGen != tg {
|
||||||
return false
|
m.mu.Unlock()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
m.appliedGen = tg
|
m.appliedGen = tg
|
||||||
return true
|
toSignal := m.pending
|
||||||
|
m.pending = nil
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
if signal && m.onConverged != nil {
|
||||||
|
for _, setAt := range toSignal {
|
||||||
|
m.onConverged(time.Since(setAt))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,30 @@ func TestMapStateManager_ConvergesThenStops(t *testing.T) {
|
|||||||
require.EqualValues(t, 3, atomic.LoadInt32(&passes), "apply must not run after convergence")
|
require.EqualValues(t, 3, atomic.LoadInt32(&passes), "apply must not run after convergence")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// every map management sends is reported via onConverged exactly once, in order
|
||||||
|
// — mirroring the legacy per-message handleSync timing.
|
||||||
|
func TestMapStateManager_SignalsEveryMap(t *testing.T) {
|
||||||
|
var converged atomic.Int32
|
||||||
|
apply := func(*mgmProto.SyncResponse) (bool, error) {
|
||||||
|
return false, nil // each map converges in one pass
|
||||||
|
}
|
||||||
|
m := newMapStateManager(apply, func(time.Duration) { converged.Add(1) })
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go m.run(ctx)
|
||||||
|
|
||||||
|
const maps = 3
|
||||||
|
for i := 0; i < maps; i++ {
|
||||||
|
require.NoError(t, m.SetTarget(&mgmProto.SyncResponse{}))
|
||||||
|
}
|
||||||
|
require.Eventually(t, func() bool { return converged.Load() == maps }, 2*time.Second, 10*time.Millisecond)
|
||||||
|
|
||||||
|
// no extra signals once the stream goes quiet
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
require.EqualValues(t, maps, converged.Load())
|
||||||
|
}
|
||||||
|
|
||||||
// an apply error drops the target: no retry of the same target, no onConverged,
|
// an apply error drops the target: no retry of the same target, no onConverged,
|
||||||
// the loop goes idle — and a fresh target is still applied afterwards.
|
// the loop goes idle — and a fresh target is still applied afterwards.
|
||||||
func TestMapStateManager_DropsTargetOnError(t *testing.T) {
|
func TestMapStateManager_DropsTargetOnError(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user