feat(config): notify reload hooks after a settings update
Build / ubuntu-latest (push) Failing after 3m8s
Build / windows-latest (push) Canceled after 4m3s

A settings update replaced the in-memory configuration but nothing told the code
that derives state from it. The logger is the first such consumer: its debug
flag is captured once at startup by postLog.SetDebugMode, so toggling
system.debugMode at runtime changed what config.IsDebugMode() reported while
DEBUG lines stayed filtered by the value from boot.

Add a hook registry to the config package. OnReload registers a callback and
every writer of config.json runs the registered hooks with the configuration now
in effect. It lives in config so packages that already depend on it (the logger,
and later the controller manager) can react without config importing them back,
which would be an import cycle.

Hooks run after settingsLock is released, not inside it. They will rebuild
controllers once those support reloading, which can wait on a network call, and
settingsLock is also held by the node tracker's background save — running a hook
under the lock would stall node registration behind a settings edit.
runSettingsUpdate now owns that lock/notify sequence for all four writer paths
(UpdateSettings and the three webhook endpoint helpers).

A panicking hook is logged and skipped: by then the new configuration is on disk
and published, so reporting the write as failed would be a lie, and the hooks
registered after the broken one still need to run.
This commit is contained in:
2026-09-28 23:10:33 +08:00
parent ac5587d0c8
commit d9a9918e21
5 changed files with 280 additions and 38 deletions
+18 -22
View File
@@ -60,13 +60,12 @@ func AddWebhookEndpoint(name string, fields map[string]interface{}) error {
return err
}
settingsLock.Lock()
defer settingsLock.Unlock()
if _, exists := webhookEndpoint(name); exists {
return fmt.Errorf("%w: %s", ErrWebhookEndpointExists, name)
}
return updateSettingsLocked(SettingGlobal, webhookEndpointsPatch(name, patch))
return runSettingsUpdate(func() (*Config, error) {
if _, exists := webhookEndpoint(name); exists {
return nil, fmt.Errorf("%w: %s", ErrWebhookEndpointExists, name)
}
return updateSettingsLocked(SettingGlobal, webhookEndpointsPatch(name, patch))
})
}
// ModifyWebhookEndpoint updates an existing incoming webhook endpoint. Only the
@@ -84,27 +83,24 @@ func ModifyWebhookEndpoint(name string, fields map[string]interface{}) error {
return fmt.Errorf("%w: no fields to update", ErrWebhookEndpointInvalid)
}
settingsLock.Lock()
defer settingsLock.Unlock()
if _, exists := webhookEndpoint(name); !exists {
return fmt.Errorf("%w: %s", ErrWebhookEndpointNotFound, name)
}
return updateSettingsLocked(SettingGlobal, webhookEndpointsPatch(name, patch))
return runSettingsUpdate(func() (*Config, error) {
if _, exists := webhookEndpoint(name); !exists {
return nil, fmt.Errorf("%w: %s", ErrWebhookEndpointNotFound, name)
}
return updateSettingsLocked(SettingGlobal, webhookEndpointsPatch(name, patch))
})
}
// DeleteWebhookEndpoint removes the incoming webhook endpoint registered under
// name. The endpoint stops accepting requests as soon as the configuration is
// reloaded.
func DeleteWebhookEndpoint(name string) error {
settingsLock.Lock()
defer settingsLock.Unlock()
if _, exists := webhookEndpoint(name); !exists {
return fmt.Errorf("%w: %s", ErrWebhookEndpointNotFound, name)
}
return updateSettingsLocked(SettingGlobal, webhookEndpointDeletePatch(name))
return runSettingsUpdate(func() (*Config, error) {
if _, exists := webhookEndpoint(name); !exists {
return nil, fmt.Errorf("%w: %s", ErrWebhookEndpointNotFound, name)
}
return updateSettingsLocked(SettingGlobal, webhookEndpointDeletePatch(name))
})
}
// webhookEndpoint returns the named endpoint held by the loaded configuration.