@marcel91200 / caddy Mirror · read-only / commits / 402f4236
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

Implement "global" state for modules, OnLoad and OnUnload callbacks

Tested for memory leaks and performance. Obviously the added locking and global state is not awesome, but the alternative is a little uglier IMO: we'd have to make some sort of "liaison" value which stores the state, then pass it around to every module, and so LoadModule becomes a lot less accessible, and each module would need to maintain a reference to it... nope, just ugly. I think this is the cleaner solution: just make sure only one Start() happens at a time, and keep global things global. Very simple log middleware is an example. Might need to reorder the operations in Start() and handle errors differently, etc. Otherwise, I'm mostly happy with this solution...

Matthew Holt committed Apr 8, 2019 at 00:00 UTC 402f423693d86d3e37daf64f369dbf54f69338b3
3 files changed +122 -10
caddy.go
+96 -6
@@ -10,13 +10,34 @@ import (
10 "time"
11 )
12
13 -var currentCfg *Config
14 -var currentCfgMu sync.Mutex
15 -
13 // Start runs Caddy with the given config.
14 func Start(cfg Config) error {
15 + // allow only one call to Start at a time,
16 + // since various calls to LoadModule()
17 + // access shared map moduleInstances
18 + startMu.Lock()
19 + defer startMu.Unlock()
20 +
21 + // prepare the config for use
22 cfg.runners = make(map[string]Runner)
23 + cfg.moduleStates = make(map[string]interface{})
24 +
25 + // reset the shared moduleInstances map; but
26 + // keep a temporary reference to the current
27 + // one so we can transfer over any necessary
28 + // state to the new modules; or in case this
29 + // function returns an error, we need to put
30 + // the "old" one back where we found it
31 + var err error
32 + oldModuleInstances := moduleInstances
33 + defer func() {
34 + if err != nil {
35 + moduleInstances = oldModuleInstances
36 + }
37 + }()
38 + moduleInstances = make(map[string][]interface{})
39
40 + // load (decode) each runner module
41 for modName, rawMsg := range cfg.Modules {
42 val, err := LoadModule(modName, rawMsg)
43 if err != nil {
@@ -34,19 +55,60 @@ func Start(cfg Config) error {
55 }
56 }
57
37 - // shut down down the old ones
58 + // shut down down the old runners
59 currentCfgMu.Lock()
60 if currentCfg != nil {
40 - for _, r := range currentCfg.runners {
61 + for name, r := range currentCfg.runners {
62 err := r.Cancel()
63 if err != nil {
43 - log.Println(err)
64 + log.Printf("[ERROR] cancel %s: %v", name, err)
65 }
66 }
67 }
68 + oldCfg := currentCfg
69 currentCfg = &cfg
70 currentCfgMu.Unlock()
71
72 + // invoke unload callbacks on old configuration
73 + for modName := range oldModuleInstances {
74 + mod, err := GetModule(modName)
75 + if err != nil {
76 + return err
77 + }
78 + if mod.OnUnload != nil {
79 + var unloadingState interface{}
80 + if oldCfg != nil {
81 + unloadingState = oldCfg.moduleStates[modName]
82 + }
83 + err := mod.OnUnload(unloadingState)
84 + if err != nil {
85 + log.Printf("[ERROR] module OnUnload: %s: %v", modName, err)
86 + continue
87 + }
88 + }
89 + }
90 +
91 + // invoke load callbacks on new configuration
92 + for modName, instances := range moduleInstances {
93 + mod, err := GetModule(modName)
94 + if err != nil {
95 + return err
96 + }
97 + if mod.OnLoad != nil {
98 + var priorState interface{}
99 + if oldCfg != nil {
100 + priorState = oldCfg.moduleStates[modName]
101 + }
102 + modState, err := mod.OnLoad(instances, priorState)
103 + if err != nil {
104 + return fmt.Errorf("module OnLoad: %s: %v", modName, err)
105 + }
106 + if modState != nil {
107 + cfg.moduleStates[modName] = modState
108 + }
109 + }
110 + }
111 +
112 // shut down listeners that are no longer being used
113 listenersMu.Lock()
114 for key, info := range listeners {
@@ -74,7 +136,15 @@ type Runner interface {
136 type Config struct {
137 TestVal string `json:"testval"`
138 Modules map[string]json.RawMessage `json:"modules"`
139 +
140 + // runners stores the decoded Modules values,
141 + // keyed by module name.
142 runners map[string]Runner
143 +
144 + // moduleStates stores the optional "global" state
145 + // values of every module used by this configuration,
146 + // keyed by module name.
147 + moduleStates map[string]interface{}
148 }
149
150 // Duration is a JSON-string-unmarshable duration type.
@@ -95,3 +165,23 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
165 func (d Duration) MarshalJSON() ([]byte, error) {
166 return []byte(fmt.Sprintf(`"%s"`, time.Duration(d).String())), nil
167 }
168 +
169 +// currentCfg is the currently-loaded configuration.
170 +var (
171 + currentCfg *Config
172 + currentCfgMu sync.Mutex
173 +)
174 +
175 +// moduleInstances stores the individual instantiated
176 +// values of modules, keyed by module name. The list
177 +// of instances of each module get passed into the
178 +// respective module's OnLoad callback, so they can
179 +// set up any global state and/or make sure their
180 +// configuration, when viewed as a whole, is valid.
181 +// Since this list is shared, only one Start() routine
182 +// must be allowed to happen at any given time.
183 +var moduleInstances = make(map[string][]interface{})
184 +
185 +// startMu ensures that only one Start() happens at a time.
186 +// This is important since
187 +var startMu sync.Mutex
modules.go
+6 -2
@@ -11,8 +11,10 @@ import (
11
12 // Module is a module.
13 type Module struct {
14 - Name string
15 - New func() (interface{}, error)
14 + Name string
15 + New func() (interface{}, error)
16 + OnLoad func(instances []interface{}, priorState interface{}) (newState interface{}, err error)
17 + OnUnload func(state interface{}) error
18 }
19
20 func (m Module) String() string { return m.Name }
@@ -145,6 +147,8 @@ func LoadModule(name string, rawMsg json.RawMessage) (interface{}, error) {
147 }
148 }
149
150 + moduleInstances[mod.Name] = append(moduleInstances[mod.Name], val)
151 +
152 return val, nil
153 }
154
modules/caddyhttp/caddylog/log.go
+20 -2
@@ -12,13 +12,31 @@ import (
12 func init() {
13 caddy2.RegisterModule(caddy2.Module{
14 Name: "http.middleware.log",
15 - New: func() (interface{}, error) { return &Log{}, nil },
15 + New: func() (interface{}, error) { return new(Log), nil },
16 + OnLoad: func(instances []interface{}, priorState interface{}) (interface{}, error) {
17 + var counter int
18 + if priorState != nil {
19 + counter = priorState.(int)
20 + }
21 + counter++
22 + for _, inst := range instances {
23 + logInst := inst.(*Log)
24 + logInst.counter = counter
25 + }
26 + log.Println("State is now:", counter)
27 + return counter, nil
28 + },
29 + OnUnload: func(state interface{}) error {
30 + log.Println("Closing log files, state:", state)
31 + return nil
32 + },
33 })
34 }
35
36 // Log implements a simple logging middleware.
37 type Log struct {
38 Filename string
39 + counter int
40 }
41
42 func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
@@ -28,7 +46,7 @@ func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.H
46 return err
47 }
48
31 - log.Println("latency:", time.Now().Sub(start))
49 + log.Println("latency:", time.Now().Sub(start), l.counter)
50
51 return nil
52 }