Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
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.
Matthew Holt committed
Sep 30, 2019 at 09:16 UTC
8eb2c3725199b17ae713dd0756a0e491e4829c12
4 files changed
+136
-99
caddy.go
+121
-94
@@ -54,105 +54,124 @@ func Run(newCfg *Config) error {
54
currentCfgMu.Lock()
55
defer currentCfgMu.Unlock()
56
57
- if newCfg != nil {
58
- // because we will need to roll back any state
59
- // modifications if this function errors, we
60
- // keep a single error value and scope all
61
- // sub-operations to their own functions to
62
- // ensure this error value does not get
63
- // overridden or missed when it should have
64
- // been set by a short assignment
65
- var err error
66
-
67
- // prepare the new config for use
68
- newCfg.apps = make(map[string]App)
69
-
70
- // create a context within which to load
71
- // modules - essentially our new config's
72
- // execution environment; be sure that
73
- // cleanup occurs when we return if there
74
- // was an error; if no error, it will get
75
- // cleaned up on next config cycle
76
- ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
77
- defer func() {
78
- if err != nil {
79
- cancel() // clean up now
80
- }
81
- }()
82
- newCfg.cancelFunc = cancel // clean up later
83
-
84
- // set up storage and make it CertMagic's default storage, too
85
- err = func() error {
86
- if newCfg.StorageRaw != nil {
87
- val, err := ctx.LoadModuleInline("module", "caddy.storage", newCfg.StorageRaw)
88
- if err != nil {
89
- return fmt.Errorf("loading storage module: %v", err)
90
- }
91
- stor, err := val.(StorageConverter).CertMagicStorage()
92
- if err != nil {
93
- return fmt.Errorf("creating storage value: %v", err)
94
- }
95
- newCfg.storage = stor
96
- newCfg.StorageRaw = nil // allow GC to deallocate
97
- }
98
- if newCfg.storage == nil {
99
- newCfg.storage = &certmagic.FileStorage{Path: dataDir()}
100
- }
101
- certmagic.Default.Storage = newCfg.storage
57
+ // run the new config and start all its apps
58
+ err := run(newCfg, true)
59
+ if err != nil {
60
+ return err
61
+ }
62
103
- return nil
104
- }()
63
+ // swap old config with the new one
64
+ oldCfg := currentCfg
65
+ currentCfg = newCfg
66
+
67
+ // Stop, Cleanup each old app
68
+ unsyncedStop(oldCfg)
69
+
70
+ return nil
71
+}
72
+
73
+// run runs newCfg and starts all its apps if
74
+// start is true. If any errors happen, cleanup
75
+// is performed if any modules were provisioned;
76
+// apps that were started already will be stopped,
77
+// so this function should not leak resources if
78
+// an error is returned.
79
+func run(newCfg *Config, start bool) error {
80
+ if newCfg == nil {
81
+ return nil
82
+ }
83
+
84
+ // because we will need to roll back any state
85
+ // modifications if this function errors, we
86
+ // keep a single error value and scope all
87
+ // sub-operations to their own functions to
88
+ // ensure this error value does not get
89
+ // overridden or missed when it should have
90
+ // been set by a short assignment
91
+ var err error
92
+
93
+ // prepare the new config for use
94
+ newCfg.apps = make(map[string]App)
95
+
96
+ // create a context within which to load
97
+ // modules - essentially our new config's
98
+ // execution environment; be sure that
99
+ // cleanup occurs when we return if there
100
+ // was an error; if no error, it will get
101
+ // cleaned up on next config cycle
102
+ ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
103
+ defer func() {
104
if err != nil {
106
- return err
105
+ cancel() // clean up now
106
}
107
+ }()
108
+ newCfg.cancelFunc = cancel // clean up later
109
109
- // Load, Provision, Validate each app and their submodules
110
- err = func() error {
111
- for modName, rawMsg := range newCfg.AppsRaw {
112
- val, err := ctx.LoadModule(modName, rawMsg)
113
- if err != nil {
114
- return fmt.Errorf("loading app module '%s': %v", modName, err)
115
- }
116
- newCfg.apps[modName] = val.(App)
110
+ // set up storage and make it CertMagic's default storage, too
111
+ err = func() error {
112
+ if newCfg.StorageRaw != nil {
113
+ val, err := ctx.LoadModuleInline("module", "caddy.storage", newCfg.StorageRaw)
114
+ if err != nil {
115
+ return fmt.Errorf("loading storage module: %v", err)
116
}
118
- return nil
119
- }()
120
- if err != nil {
121
- return err
117
+ stor, err := val.(StorageConverter).CertMagicStorage()
118
+ if err != nil {
119
+ return fmt.Errorf("creating storage value: %v", err)
120
+ }
121
+ newCfg.storage = stor
122
+ newCfg.StorageRaw = nil // allow GC to deallocate
123
+ }
124
+ if newCfg.storage == nil {
125
+ newCfg.storage = &certmagic.FileStorage{Path: dataDir()}
126
}
127
+ certmagic.Default.Storage = newCfg.storage
128
124
- // Start
125
- err = func() error {
126
- var started []string
127
- for name, a := range newCfg.apps {
128
- err := a.Start()
129
- if err != nil {
130
- for _, otherAppName := range started {
131
- err2 := newCfg.apps[otherAppName].Stop()
132
- if err2 != nil {
133
- err = fmt.Errorf("%v; additionally, aborting app %s: %v",
134
- err, otherAppName, err2)
135
- }
136
- }
137
- return fmt.Errorf("%s app module: start: %v", name, err)
138
- }
139
- started = append(started, name)
129
+ return nil
130
+ }()
131
+ if err != nil {
132
+ return err
133
+ }
134
+
135
+ // Load, Provision, Validate each app and their submodules
136
+ err = func() error {
137
+ for modName, rawMsg := range newCfg.AppsRaw {
138
+ val, err := ctx.LoadModule(modName, rawMsg)
139
+ if err != nil {
140
+ return fmt.Errorf("loading app module '%s': %v", modName, err)
141
}
141
- return nil
142
- }()
143
- if err != nil {
144
- return err
142
+ newCfg.apps[modName] = val.(App)
143
}
144
+ return nil
145
+ }()
146
+ if err != nil {
147
+ return err
148
}
149
148
- // swap old config with the new one
149
- oldCfg := currentCfg
150
- currentCfg = newCfg
151
-
152
- // Stop, Cleanup each old app
153
- unsyncedStop(oldCfg)
150
+ if !start {
151
+ return nil
152
+ }
153
155
- return nil
154
+ // Start
155
+ return func() error {
156
+ var started []string
157
+ for name, a := range newCfg.apps {
158
+ err := a.Start()
159
+ if err != nil {
160
+ // an app failed to start, so we need to stop
161
+ // all other apps that were already started
162
+ for _, otherAppName := range started {
163
+ err2 := newCfg.apps[otherAppName].Stop()
164
+ if err2 != nil {
165
+ err = fmt.Errorf("%v; additionally, aborting app %s: %v",
166
+ err, otherAppName, err2)
167
+ }
168
+ }
169
+ return fmt.Errorf("%s app module: start: %v", name, err)
170
+ }
171
+ started = append(started, name)
172
+ }
173
+ return nil
174
+ }()
175
}
176
177
// Stop stops running the current configuration.
@@ -168,26 +187,34 @@ func Stop() error {
187
return nil
188
}
189
171
-// unsyncedStop stops oldCfg from running, but if
190
+// unsyncedStop stops cfg from running, but if
191
// applicable, you need to acquire locks yourself.
173
-// It is a no-op if oldCfg is nil. If any app
192
+// It is a no-op if cfg is nil. If any app
193
// returns an error when stopping, it is logged
194
// and the function continues with the next app.
176
-func unsyncedStop(oldCfg *Config) {
177
- if oldCfg == nil {
195
+// This function assumes all apps in cfg were
196
+// successfully started.
197
+func unsyncedStop(cfg *Config) {
198
+ if cfg == nil {
199
return
200
}
201
202
// stop each app
182
- for name, a := range oldCfg.apps {
203
+ for name, a := range cfg.apps {
204
err := a.Stop()
205
if err != nil {
206
log.Printf("[ERROR] stop %s: %v", name, err)
207
}
208
}
209
189
- // clean up all old modules
190
- oldCfg.cancelFunc()
210
+ // clean up all modules
211
+ cfg.cancelFunc()
212
+}
213
+
214
+// Validate loads, provisions, and validates
215
+// cfg, but does not start running it.
216
+func Validate(cfg *Config) error {
217
+ return run(cfg, false)
218
}
219
220
// Duration is a JSON-string-unmarshable duration type.
context.go
+9
@@ -131,6 +131,14 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{},
131
if prov, ok := val.(Provisioner); ok {
132
err := prov.Provision(ctx)
133
if err != nil {
134
+ // incomplete provisioning could have left state
135
+ // dangling, so make sure it gets cleaned up
136
+ if cleanerUpper, ok := val.(CleanerUpper); ok {
137
+ err2 := cleanerUpper.Cleanup()
138
+ if err2 != nil {
139
+ err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
140
+ }
141
+ }
142
return nil, fmt.Errorf("provision %s: %v", mod.Name, err)
143
}
144
}
@@ -138,6 +146,7 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{},
146
if validator, ok := val.(Validator); ok {
147
err := validator.Validate()
148
if err != nil {
149
+ // since the module was already provisioned, make sure we clean up
150
if cleanerUpper, ok := val.(CleanerUpper); ok {
151
err2 := cleanerUpper.Cleanup()
152
if err2 != nil {
modules.go
+3
-2
@@ -253,9 +253,10 @@ type Validator interface {
253
254
// CleanerUpper is implemented by modules which may have side-effects
255
// such as opened files, spawned goroutines, or allocated some sort
256
-// of non-local state when they were provisioned. This method should
256
+// of non-stack state when they were provisioned. This method should
257
// deallocate/cleanup those resources to prevent memory leaks. Cleanup
258
-// should be fast and efficient.
258
+// should be fast and efficient. Cleanup should work even if Provision
259
+// returns an error, to allow cleaning up from partial provisionings.
260
type CleanerUpper interface {
261
Cleanup() error
262
}
sigtrap.go
+3
-3
@@ -65,9 +65,9 @@ func gracefulStop(sigName string) {
65
os.Exit(exitCode)
66
}
67
68
-// Exit codes. Generally, you will want to avoid
69
-// automatically restarting the process if the
70
-// exit code is 1.
68
+// Exit codes. Generally, you should NOT
69
+// automatically restart the process if the
70
+// exit code is ExitCodeFailedStartup (1).
71
const (
72
ExitCodeSuccess = iota
73
ExitCodeFailedStartup