@marcel91200 / caddy Mirror · read-only / commits / 8eb2c372
Fork 0

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