summaryrefslogtreecommitdiff
path: root/caddy.go
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2019-09-30 09:16:01 -0600
committerMatthew Holt <mholt@users.noreply.github.com>2019-09-30 09:16:01 -0600
commit8eb2c3725199b17ae713dd0756a0e491e4829c12 (patch)
treed368a149518ed7247de4a2571e41aa7a6dbb38c7 /caddy.go
parent1e662262179d326586d2beb849f842b82b7324c4 (diff)
Clean up provisioned modules on error; refactor Run(); add Validate()
Modules that return an error during provisioning should still be cleaned up so that they don't leak any resources they may have allocated before the error occurred. Cleanup should be able to run even if Provision does not complete fully.
Diffstat (limited to 'caddy.go')
-rw-r--r--caddy.go215
1 files changed, 121 insertions, 94 deletions
diff --git a/caddy.go b/caddy.go
index b91ae79..fb3e488 100644
--- a/caddy.go
+++ b/caddy.go
@@ -54,105 +54,124 @@ func Run(newCfg *Config) error {
currentCfgMu.Lock()
defer currentCfgMu.Unlock()
- if newCfg != nil {
- // because we will need to roll back any state
- // modifications if this function errors, we
- // keep a single error value and scope all
- // sub-operations to their own functions to
- // ensure this error value does not get
- // overridden or missed when it should have
- // been set by a short assignment
- var err error
-
- // prepare the new config for use
- newCfg.apps = make(map[string]App)
-
- // create a context within which to load
- // modules - essentially our new config's
- // execution environment; be sure that
- // cleanup occurs when we return if there
- // was an error; if no error, it will get
- // cleaned up on next config cycle
- ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
- defer func() {
- if err != nil {
- cancel() // clean up now
- }
- }()
- newCfg.cancelFunc = cancel // clean up later
-
- // set up storage and make it CertMagic's default storage, too
- err = func() error {
- if newCfg.StorageRaw != nil {
- val, err := ctx.LoadModuleInline("module", "caddy.storage", newCfg.StorageRaw)
- if err != nil {
- return fmt.Errorf("loading storage module: %v", err)
- }
- stor, err := val.(StorageConverter).CertMagicStorage()
- if err != nil {
- return fmt.Errorf("creating storage value: %v", err)
- }
- newCfg.storage = stor
- newCfg.StorageRaw = nil // allow GC to deallocate
- }
- if newCfg.storage == nil {
- newCfg.storage = &certmagic.FileStorage{Path: dataDir()}
- }
- certmagic.Default.Storage = newCfg.storage
+ // run the new config and start all its apps
+ err := run(newCfg, true)
+ if err != nil {
+ return err
+ }
- return nil
- }()
+ // swap old config with the new one
+ oldCfg := currentCfg
+ currentCfg = newCfg
+
+ // Stop, Cleanup each old app
+ unsyncedStop(oldCfg)
+
+ return nil
+}
+
+// run runs newCfg and starts all its apps if
+// start is true. If any errors happen, cleanup
+// is performed if any modules were provisioned;
+// apps that were started already will be stopped,
+// so this function should not leak resources if
+// an error is returned.
+func run(newCfg *Config, start bool) error {
+ if newCfg == nil {
+ return nil
+ }
+
+ // because we will need to roll back any state
+ // modifications if this function errors, we
+ // keep a single error value and scope all
+ // sub-operations to their own functions to
+ // ensure this error value does not get
+ // overridden or missed when it should have
+ // been set by a short assignment
+ var err error
+
+ // prepare the new config for use
+ newCfg.apps = make(map[string]App)
+
+ // create a context within which to load
+ // modules - essentially our new config's
+ // execution environment; be sure that
+ // cleanup occurs when we return if there
+ // was an error; if no error, it will get
+ // cleaned up on next config cycle
+ ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
+ defer func() {
if err != nil {
- return err
+ cancel() // clean up now
}
+ }()
+ newCfg.cancelFunc = cancel // clean up later
- // Load, Provision, Validate each app and their submodules
- err = func() error {
- for modName, rawMsg := range newCfg.AppsRaw {
- val, err := ctx.LoadModule(modName, rawMsg)
- if err != nil {
- return fmt.Errorf("loading app module '%s': %v", modName, err)
- }
- newCfg.apps[modName] = val.(App)
+ // set up storage and make it CertMagic's default storage, too
+ err = func() error {
+ if newCfg.StorageRaw != nil {
+ val, err := ctx.LoadModuleInline("module", "caddy.storage", newCfg.StorageRaw)
+ if err != nil {
+ return fmt.Errorf("loading storage module: %v", err)
}
- return nil
- }()
- if err != nil {
- return err
+ stor, err := val.(StorageConverter).CertMagicStorage()
+ if err != nil {
+ return fmt.Errorf("creating storage value: %v", err)
+ }
+ newCfg.storage = stor
+ newCfg.StorageRaw = nil // allow GC to deallocate
+ }
+ if newCfg.storage == nil {
+ newCfg.storage = &certmagic.FileStorage{Path: dataDir()}
}
+ certmagic.Default.Storage = newCfg.storage
- // Start
- err = func() error {
- var started []string
- for name, a := range newCfg.apps {
- err := a.Start()
- if err != nil {
- for _, otherAppName := range started {
- err2 := newCfg.apps[otherAppName].Stop()
- if err2 != nil {
- err = fmt.Errorf("%v; additionally, aborting app %s: %v",
- err, otherAppName, err2)
- }
- }
- return fmt.Errorf("%s app module: start: %v", name, err)
- }
- started = append(started, name)
+ return nil
+ }()
+ if err != nil {
+ return err
+ }
+
+ // Load, Provision, Validate each app and their submodules
+ err = func() error {
+ for modName, rawMsg := range newCfg.AppsRaw {
+ val, err := ctx.LoadModule(modName, rawMsg)
+ if err != nil {
+ return fmt.Errorf("loading app module '%s': %v", modName, err)
}
- return nil
- }()
- if err != nil {
- return err
+ newCfg.apps[modName] = val.(App)
}
+ return nil
+ }()
+ if err != nil {
+ return err
}
- // swap old config with the new one
- oldCfg := currentCfg
- currentCfg = newCfg
-
- // Stop, Cleanup each old app
- unsyncedStop(oldCfg)
+ if !start {
+ return nil
+ }
- return nil
+ // Start
+ return func() error {
+ var started []string
+ for name, a := range newCfg.apps {
+ err := a.Start()
+ if err != nil {
+ // an app failed to start, so we need to stop
+ // all other apps that were already started
+ for _, otherAppName := range started {
+ err2 := newCfg.apps[otherAppName].Stop()
+ if err2 != nil {
+ err = fmt.Errorf("%v; additionally, aborting app %s: %v",
+ err, otherAppName, err2)
+ }
+ }
+ return fmt.Errorf("%s app module: start: %v", name, err)
+ }
+ started = append(started, name)
+ }
+ return nil
+ }()
}
// Stop stops running the current configuration.
@@ -168,26 +187,34 @@ func Stop() error {
return nil
}
-// unsyncedStop stops oldCfg from running, but if
+// unsyncedStop stops cfg from running, but if
// applicable, you need to acquire locks yourself.
-// It is a no-op if oldCfg is nil. If any app
+// It is a no-op if cfg is nil. If any app
// returns an error when stopping, it is logged
// and the function continues with the next app.
-func unsyncedStop(oldCfg *Config) {
- if oldCfg == nil {
+// This function assumes all apps in cfg were
+// successfully started.
+func unsyncedStop(cfg *Config) {
+ if cfg == nil {
return
}
// stop each app
- for name, a := range oldCfg.apps {
+ for name, a := range cfg.apps {
err := a.Stop()
if err != nil {
log.Printf("[ERROR] stop %s: %v", name, err)
}
}
- // clean up all old modules
- oldCfg.cancelFunc()
+ // clean up all modules
+ cfg.cancelFunc()
+}
+
+// Validate loads, provisions, and validates
+// cfg, but does not start running it.
+func Validate(cfg *Config) error {
+ return run(cfg, false)
}
// Duration is a JSON-string-unmarshable duration type.