core: Retry dynamic config load if config unchanged

(see discussion in #4603)
This commit is contained in:
Matthew Holt 2022-03-03 21:41:51 -07:00
parent a6199cf814
commit a72acd21b0
No known key found for this signature in database
GPG Key ID: 2A349DD577D586A5
2 changed files with 34 additions and 15 deletions

View File

@ -938,7 +938,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
err := changeConfig(r.Method, r.URL.Path, body, forceReload)
if err != nil {
if err != nil && !errors.Is(err, errSameConfig) {
return err
}

View File

@ -18,6 +18,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
@ -110,14 +111,19 @@ func Load(cfgJSON []byte, forceReload bool) error {
}
}()
return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
err := changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
if errors.Is(err, errSameConfig) {
err = nil // not really an error
}
return err
}
// changeConfig changes the current config (rawCfg) according to the
// method, traversed via the given path, and uses the given input as
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
// If the resulting config is the same as the previous, no reload will
// occur unless forceReload is true. This function is safe for
// occur unless forceReload is true. If the config is unchanged and not
// forcefully reloaded, then errConfigUnchanged This function is safe for
// concurrent use.
func changeConfig(method, path string, input []byte, forceReload bool) error {
switch method {
@ -149,7 +155,7 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
// if nothing changed, no need to do a whole reload unless the client forces it
if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
Log().Info("config is unchanged")
return nil
return errSameConfig
}
// find any IDs in this config and index them
@ -492,39 +498,47 @@ func finishSettingUp(ctx Context, cfg *Config) error {
zap.String("module", val.(Module).CaddyModule().ID.Name()),
zap.Int("load_delay", int(cfg.Admin.Config.LoadDelay)))
runLoadedConfig := func(config []byte) {
runLoadedConfig := func(config []byte) error {
logger.Info("applying dynamically-loaded config")
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, false)
if err == nil {
logger.Info("successfully applied dynamically-loaded config")
} else {
logger.Error("failed to run dynamically-loaded config", zap.Error(err))
if errors.Is(err, errSameConfig) {
return err
}
if err != nil {
logger.Error("failed to run dynamically-loaded config", zap.Error(err))
return err
}
logger.Info("successfully applied dynamically-loaded config")
return nil
}
if cfg.Admin.Config.LoadDelay > 0 {
go func() {
// the loop is here only to iterate if there is an error or a no-op config load,
// in which case we simply wait the delay and try again
// the loop is here to iterate ONLY if there is an error, a no-op config load,
// or an unchanged config; in which case we simply wait the delay and try again
for {
timer := time.NewTimer(time.Duration(cfg.Admin.Config.LoadDelay))
select {
case <-timer.C:
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
if err != nil {
Log().Error("failed loading dynamic config; will retry", zap.Error(err))
logger.Error("failed loading dynamic config; will retry", zap.Error(err))
continue
}
if loadedConfig == nil {
Log().Info("dynamically-loaded config was nil; will retry")
logger.Info("dynamically-loaded config was nil; will retry")
continue
}
err = runLoadedConfig(loadedConfig)
if errors.Is(err, errSameConfig) {
logger.Info("dynamically-loaded config was unchanged; will retry")
continue
}
runLoadedConfig(loadedConfig)
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
Log().Info("stopping dynamic config loading")
logger.Info("stopping dynamic config loading")
}
break
}
@ -798,5 +812,10 @@ var (
rawCfgIndex map[string]string
)
// errSameConfig is returned if the new config is the same
// as the old one. This isn't usually an actual, actionable
// error; it's mostly a sentinel value.
var errSameConfig = errors.New("config is unchanged")
// ImportPath is the package import path for Caddy core.
const ImportPath = "github.com/caddyserver/caddy/v2"