diff --git a/client/internal/engine.go b/client/internal/engine.go index 3f244ced7..d73beb400 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1075,6 +1075,7 @@ func (e *Engine) applySyncPass(update *mgmProto.SyncResponse, firstPass bool) (b // when the update carries one. A config-only update leaves syncTargetMap nil. The // caller holds syncMsgMux. func (e *Engine) loadSyncTarget(update *mgmProto.SyncResponse) error { + pending := e.syncTargetMap e.syncTargetMap = nil e.ApplySessionDeadline(update.GetSessionExpiresAt()) @@ -1158,6 +1159,14 @@ func (e *Engine) loadSyncTarget(update *mgmProto.SyncResponse) error { e.latestComponents = components } + // updateNetworkMap ignores a stale map and reports convergence, which would + // strand the remaining peer batches of a newer map still being applied. + if pending != nil && nm.GetSerial() < e.networkSerial { + log.Debugf("ignoring network map with serial %d while serial %d is converging", nm.GetSerial(), e.networkSerial) + e.syncTargetMap = pending + return nil + } + e.syncTargetMap = nm return nil } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 289d6b2bc..6173e99d7 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -1798,3 +1798,23 @@ func TestEngine_ApplySyncPassDecodesOncePerTarget(t *testing.T) { _, err = e.applySyncPass(undecodable, true) assert.Error(t, err, "the first pass of a target decodes it") } + +// A map older than the one still converging must not take its place: the +// stale guard in updateNetworkMap would ignore it and report convergence, +// leaving the newer map's remaining peer batches unapplied. +func TestEngine_LoadSyncTargetKeepsNewerPendingMap(t *testing.T) { + pending := &mgmtProto.NetworkMap{Serial: 10} + e := &Engine{ + ctx: context.Background(), + syncMsgMux: &sync.Mutex{}, + networkSerial: 10, + syncTargetMap: pending, + } + + require.NoError(t, e.loadSyncTarget(&mgmtProto.SyncResponse{NetworkMap: &mgmtProto.NetworkMap{Serial: 9}})) + assert.Same(t, pending, e.syncTargetMap, "a stale map should leave the pending newer map in place") + + newer := &mgmtProto.NetworkMap{Serial: 11} + require.NoError(t, e.loadSyncTarget(&mgmtProto.SyncResponse{NetworkMap: newer})) + assert.Same(t, newer, e.syncTargetMap, "a newer map should replace the pending one") +}