Skip to content

Commit a4756ab

Browse files
Use config snapshots for management reload
1 parent 7f026e1 commit a4756ab

4 files changed

Lines changed: 43 additions & 25 deletions

File tree

internal/api/handlers/management/auth_files.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
geminiAuth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/gemini"
2929
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/kimi"
3030
xaiauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/xai"
31-
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
3231
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
3332
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
3433
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
@@ -1267,13 +1266,11 @@ func (h *Handler) PatchAuthFileStatus(c *gin.Context) {
12671266
c.JSON(http.StatusNotFound, gin.H{"error": "config api key entry not found"})
12681267
return
12691268
}
1270-
if errSave := config.SaveConfigPreserveComments(h.configFilePath, h.cfg); errSave != nil {
1271-
h.mu.Unlock()
1272-
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save config: %v", errSave)})
1269+
cfgSnapshot, okSnapshot := h.saveConfigAndSnapshotLocked(c)
1270+
h.mu.Unlock()
1271+
if !okSnapshot {
12731272
return
12741273
}
1275-
cfgSnapshot := h.cfg
1276-
h.mu.Unlock()
12771274
h.reloadConfigAfterManagementSave(ctx, cfgSnapshot)
12781275
if h.tokenStore != nil {
12791276
_ = h.tokenStore.Delete(ctx, targetAuth.ID)

internal/api/handlers/management/handler.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,25 +152,48 @@ func (h *Handler) SetConfigReloadHook(hook func(context.Context, *config.Config)
152152
h.mu.Unlock()
153153
}
154154

155-
func (h *Handler) reloadConfigAfterManagementSave(ctx context.Context, cfg *config.Config) {
156-
if h == nil || cfg == nil {
155+
// snapshotConfigLocked clones the full runtime config while h.mu is held.
156+
// Callers must hold h.mu.
157+
func (h *Handler) snapshotConfigLocked() *config.Config {
158+
if h == nil || h.cfg == nil {
159+
return nil
160+
}
161+
return h.cfg.CloneForRuntime()
162+
}
163+
164+
// saveConfigAndSnapshotLocked saves h.cfg and returns a full runtime config snapshot.
165+
// Callers must hold h.mu.
166+
func (h *Handler) saveConfigAndSnapshotLocked(c *gin.Context) (*config.Config, bool) {
167+
if errSave := config.SaveConfigPreserveComments(h.configFilePath, h.cfg); errSave != nil {
168+
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save config: %v", errSave)})
169+
return nil, false
170+
}
171+
return h.snapshotConfigLocked(), true
172+
}
173+
174+
// reloadConfigAfterManagementSave reloads from an independent config snapshot.
175+
// 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 {
157178
return
158179
}
159180
h.mu.Lock()
160181
hook := h.configReloadHook
161182
host := h.pluginHost
162183
h.mu.Unlock()
163184
if hook != nil {
164-
hook(ctx, cfg)
185+
hook(ctx, cfgSnapshot)
165186
return
166187
}
167188
if host != nil {
168-
host.ApplyConfig(ctx, cfg)
189+
host.ApplyConfig(ctx, cfgSnapshot)
169190
}
170191
}
171192

172-
func (h *Handler) reloadConfigAfterManagementSaveAsync(ctx context.Context, cfg *config.Config) {
173-
if h == nil || cfg == nil {
193+
// reloadConfigAfterManagementSaveAsync reloads from an independent config snapshot.
194+
// 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 {
174197
return
175198
}
176199
reloadCtx := context.Background()
@@ -183,7 +206,7 @@ func (h *Handler) reloadConfigAfterManagementSaveAsync(ctx context.Context, cfg
183206
log.WithField("panic", recovered).Error("management: async config reload panicked")
184207
}
185208
}()
186-
h.reloadConfigAfterManagementSave(reloadCtx, cfg)
209+
h.reloadConfigAfterManagementSave(reloadCtx, cfgSnapshot)
187210
}()
188211
}
189212

internal/api/handlers/management/plugin_store.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,9 @@ func (h *Handler) installPluginFromStore(c *gin.Context, goos, goarch string) {
226226
if errInstall != nil {
227227
if unloadedBeforeWrite {
228228
h.mu.Lock()
229-
reloadCfg := h.cfg
229+
cfgSnapshot := h.snapshotConfigLocked()
230230
h.mu.Unlock()
231-
h.reloadConfigAfterManagementSave(c.Request.Context(), reloadCfg)
231+
h.reloadConfigAfterManagementSave(c.Request.Context(), cfgSnapshot)
232232
}
233233
if errors.Is(errInstall, pluginstore.ErrLoadedPluginLocked) {
234234
c.JSON(http.StatusConflict, gin.H{
@@ -271,10 +271,10 @@ func (h *Handler) installPluginFromStore(c *gin.Context, goos, goarch string) {
271271
})
272272
return
273273
}
274-
reloadCfg := h.cfg
274+
cfgSnapshot := h.snapshotConfigLocked()
275275
h.mu.Unlock()
276276

277-
h.reloadConfigAfterManagementSaveAsync(c.Request.Context(), reloadCfg)
277+
h.reloadConfigAfterManagementSaveAsync(c.Request.Context(), cfgSnapshot)
278278
log.WithFields(log.Fields{
279279
"plugin_id": result.ID,
280280
"source_id": source.ID,

internal/api/handlers/management/plugins.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,13 @@ func (h *Handler) PatchPluginEnabled(c *gin.Context) {
226226
return
227227
}
228228
h.cfg.Plugins.Configs[id] = updated
229-
if errSave := config.SaveConfigPreserveComments(h.configFilePath, h.cfg); errSave != nil {
230-
h.mu.Unlock()
231-
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save config: %v", errSave)})
229+
cfgSnapshot, okSnapshot := h.saveConfigAndSnapshotLocked(c)
230+
h.mu.Unlock()
231+
if !okSnapshot {
232232
return
233233
}
234-
reloadCfg := h.cfg
235-
h.mu.Unlock()
236234

237-
h.reloadConfigAfterManagementSaveAsync(c.Request.Context(), reloadCfg)
235+
h.reloadConfigAfterManagementSaveAsync(c.Request.Context(), cfgSnapshot)
238236
c.JSON(http.StatusOK, gin.H{"status": "ok"})
239237
}
240238

@@ -375,10 +373,10 @@ func (h *Handler) DeletePlugin(c *gin.Context) {
375373
return
376374
}
377375
}
378-
reloadCfg := h.cfg
376+
cfgSnapshot := h.snapshotConfigLocked()
379377
h.mu.Unlock()
380378

381-
h.reloadConfigAfterManagementSaveAsync(c.Request.Context(), reloadCfg)
379+
h.reloadConfigAfterManagementSaveAsync(c.Request.Context(), cfgSnapshot)
382380
c.JSON(http.StatusOK, gin.H{
383381
"status": "deleted",
384382
"id": htmlsanitize.String(id),

0 commit comments

Comments
 (0)