package config import ( "fmt" "sync" "nukumizu-backend/postLog" ) // reloadHooks are the callbacks run after the global configuration has been // reloaded, i.e. after every settings update that touches config.json. // // They exist so that packages which already depend on config — the controller // manager, the logger — can react to an update without config importing them, // which would be an import cycle. Everything a hook needs is passed in. var ( reloadHooksMu sync.Mutex reloadHooks []func(*Config) // reloadRunMu serializes hook execution. A hook mutates process-wide state // — the logger's debug flag, the controller registry — so two overlapping // settings updates must not run them at the same time, or both would decide // to rebuild the controllers from their own view of what was applied. reloadRunMu sync.Mutex ) // OnReload registers a hook to run after every reload of the global // configuration, receiving the configuration now in effect. Hooks run in // registration order. // // Register once, at startup, before the first settings update can arrive: a // hook registered later has already missed the updates that came before it, and // the configuration it would have seen is not replayed. // // A hook runs on the goroutine serving /api/settings/set, so it must not block // for long. It may be called concurrently by two overlapping updates. func OnReload(hook func(*Config)) { reloadHooksMu.Lock() defer reloadHooksMu.Unlock() reloadHooks = append(reloadHooks, hook) } // notifyReload runs every registered hook with cfg. A nil cfg is the signal // that the update touched one of the other settings files, which have no // hook-visible reload, and it is ignored. // // A panicking hook is logged and skipped rather than allowed to unwind through // UpdateSettings: by the time hooks run the new configuration is already on // disk and published in memory, so reporting the write as failed would be a // lie, and the hooks registered after the broken one must still run. func notifyReload(cfg *Config) { if cfg == nil { return } // Copy under the registry lock, then release it before running anything: a // hook is free to register another hook without deadlocking. reloadHooksMu.Lock() hooks := make([]func(*Config), len(reloadHooks)) copy(hooks, reloadHooks) reloadHooksMu.Unlock() reloadRunMu.Lock() defer reloadRunMu.Unlock() for _, hook := range hooks { runReloadHook(hook, cfg) } } // runReloadHook runs one hook, isolating a panic to that hook. Recovering in a // separate function rather than inline is deliberate: a deferred recover placed // in the loop body would not run until notifyReload itself returned, which // would abandon the remaining hooks. func runReloadHook(hook func(*Config), cfg *Config) { defer func() { if r := recover(); r != nil { postLog.Error(fmt.Sprintf("Configuration reload hook panicked: %v", r)) } }() hook(cfg) }