@@ -38,25 +38,33 @@ const attemptMaxIdleTime = 2 * time.Hour
3838
3939// Handler aggregates config reference, persistence path and helpers.
4040type Handler struct {
41- cfg * config.Config
42- configFilePath string
43- mu sync.Mutex
44- attemptsMu sync.Mutex
45- failedAttempts map [string ]* attemptInfo // keyed by client IP
46- authManager * coreauth.Manager
47- tokenStore coreauth.Store
48- localPassword string
49- allowRemoteOverride bool
50- envSecret string
51- logDir string
52- postAuthHook coreauth.PostAuthHook
53- postAuthPersistHook coreauth.PostAuthHook
54- pluginHost * pluginhost.Host
55- configReloadHook func (context.Context , * config.Config )
56- pluginStoreRegistryURL string
57- pluginStoreHTTPClient pluginstore.HTTPDoer
58- pluginReleaseCacheMu sync.Mutex
59- pluginReleaseCache map [string ]pluginReleaseCacheEntry
41+ cfg * config.Config
42+ configFilePath string
43+ mu sync.Mutex
44+ reloadMu sync.Mutex
45+ reloadGeneration uint64
46+ appliedReloadGeneration uint64
47+ attemptsMu sync.Mutex
48+ failedAttempts map [string ]* attemptInfo // keyed by client IP
49+ authManager * coreauth.Manager
50+ tokenStore coreauth.Store
51+ localPassword string
52+ allowRemoteOverride bool
53+ envSecret string
54+ logDir string
55+ postAuthHook coreauth.PostAuthHook
56+ postAuthPersistHook coreauth.PostAuthHook
57+ pluginHost * pluginhost.Host
58+ configReloadHook func (context.Context , * config.Config )
59+ pluginStoreRegistryURL string
60+ pluginStoreHTTPClient pluginstore.HTTPDoer
61+ pluginReleaseCacheMu sync.Mutex
62+ pluginReleaseCache map [string ]pluginReleaseCacheEntry
63+ }
64+
65+ type configReloadSnapshot struct {
66+ cfg * config.Config
67+ generation uint64
6068}
6169
6270// NewHandler creates a new management handler instance.
@@ -152,48 +160,63 @@ func (h *Handler) SetConfigReloadHook(hook func(context.Context, *config.Config)
152160 h .mu .Unlock ()
153161}
154162
155- // snapshotConfigLocked clones the full runtime config while h.mu is held .
163+ // reloadSnapshotConfigLocked clones the runtime config and assigns a reload generation .
156164// Callers must hold h.mu.
157- func (h * Handler ) snapshotConfigLocked () * config. Config {
165+ func (h * Handler ) reloadSnapshotConfigLocked () configReloadSnapshot {
158166 if h == nil || h .cfg == nil {
159- return nil
167+ return configReloadSnapshot {}
168+ }
169+ h .reloadGeneration ++
170+ return configReloadSnapshot {
171+ cfg : h .cfg .CloneForRuntime (),
172+ generation : h .reloadGeneration ,
160173 }
161- return h .cfg .CloneForRuntime ()
162174}
163175
164176// saveConfigAndSnapshotLocked saves h.cfg and returns a full runtime config snapshot.
165177// Callers must hold h.mu.
166- func (h * Handler ) saveConfigAndSnapshotLocked (c * gin.Context ) (* config. Config , bool ) {
178+ func (h * Handler ) saveConfigAndSnapshotLocked (c * gin.Context ) (configReloadSnapshot , bool ) {
167179 if errSave := config .SaveConfigPreserveComments (h .configFilePath , h .cfg ); errSave != nil {
168180 c .JSON (http .StatusInternalServerError , gin.H {"error" : fmt .Sprintf ("failed to save config: %v" , errSave )})
169- return nil , false
181+ return configReloadSnapshot {} , false
170182 }
171- return h .snapshotConfigLocked (), true
183+ return h .reloadSnapshotConfigLocked (), true
172184}
173185
174186// reloadConfigAfterManagementSave reloads from an independent config snapshot.
175187// Callers must pass a full Config clone captured immediately after a successful save.
176- func (h * Handler ) reloadConfigAfterManagementSave (ctx context.Context , cfgSnapshot * config. Config ) {
177- if h == nil || cfgSnapshot == nil {
188+ func (h * Handler ) reloadConfigAfterManagementSave (ctx context.Context , snapshot configReloadSnapshot ) {
189+ if h == nil || snapshot . cfg == nil || snapshot . generation == 0 {
178190 return
179191 }
192+ h .reloadMu .Lock ()
193+ defer h .reloadMu .Unlock ()
194+
180195 h .mu .Lock ()
196+ if snapshot .generation < h .appliedReloadGeneration {
197+ h .mu .Unlock ()
198+ return
199+ }
181200 hook := h .configReloadHook
182201 host := h .pluginHost
183202 h .mu .Unlock ()
184203 if hook != nil {
185- hook (ctx , cfgSnapshot )
186- return
204+ hook (ctx , snapshot .cfg )
205+ } else if host != nil {
206+ host .ApplyConfig (ctx , snapshot .cfg )
187207 }
188- if host != nil {
189- host .ApplyConfig (ctx , cfgSnapshot )
208+
209+ h .mu .Lock ()
210+ if snapshot .generation > h .appliedReloadGeneration {
211+ h .appliedReloadGeneration = snapshot .generation
190212 }
213+ h .mu .Unlock ()
191214}
192215
193216// reloadConfigAfterManagementSaveAsync reloads from an independent config snapshot.
194217// Callers must pass a full Config clone captured immediately after a successful save.
195- func (h * Handler ) reloadConfigAfterManagementSaveAsync (ctx context.Context , cfgSnapshot * config. Config ) {
196- if h == nil || cfgSnapshot == nil {
218+ func (h * Handler ) reloadConfigAfterManagementSaveAsync (ctx context.Context , snapshot configReloadSnapshot ) {
219+ if h == nil || snapshot . cfg == nil || snapshot . generation == 0 {
197220 return
198221 }
199222 reloadCtx := context .Background ()
@@ -206,7 +229,7 @@ func (h *Handler) reloadConfigAfterManagementSaveAsync(ctx context.Context, cfgS
206229 log .WithField ("panic" , recovered ).Error ("management: async config reload panicked" )
207230 }
208231 }()
209- h .reloadConfigAfterManagementSave (reloadCtx , cfgSnapshot )
232+ h .reloadConfigAfterManagementSave (reloadCtx , snapshot )
210233 }()
211234}
212235
0 commit comments