github.com/kaydxh/golang@v0.0.131/pkg/webserver/hooks.go (about) 1 /* 2 *Copyright (c) 2022, kaydxh 3 * 4 *Permission is hereby granted, free of charge, to any person obtaining a copy 5 *of this software and associated documentation files (the "Software"), to deal 6 *in the Software without restriction, including without limitation the rights 7 *to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 *copies of the Software, and to permit persons to whom the Software is 9 *furnished to do so, subject to the following conditions: 10 * 11 *The above copyright notice and this permission notice shall be included in all 12 *copies or substantial portions of the Software. 13 * 14 *THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 *IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 *FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 *AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 *LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 *OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 *SOFTWARE. 21 */ 22 package webserver 23 24 import ( 25 "context" 26 "fmt" 27 "runtime/debug" 28 29 errors_ "github.com/kaydxh/golang/go/errors" 30 "github.com/sirupsen/logrus" 31 ) 32 33 // PostStartHookFunc is a function that is called after the server has started. 34 // It must properly handle cases like: 35 // 1. asynchronous start in multiple API server processes 36 // 2. conflicts between the different processes all trying to perform the same action 37 // 3. partially complete work (API server crashes while running your hook) 38 // 4. API server access **BEFORE** your hook has completed 39 // Think of it like a mini-controller that is super privileged and gets to run in-process 40 // If you use this feature, tag @deads2k on github who has promised to review code for anyone's PostStartHook 41 // until it becomes easier to use. 42 type PostStartHookFunc func(ctx context.Context) error 43 44 // PreShutdownHookFunc is a function that can be added to the shutdown logic. 45 type PreShutdownHookFunc func() error 46 47 // PostStartHookProvider is an interface in addition to provide a post start hook for the api server 48 type PostStartHookProvider interface { 49 PostStartHook() (string, PostStartHookFunc, error) 50 } 51 52 type postStartHookEntry struct { 53 hook PostStartHookFunc 54 // originatingStack holds the stack that registered postStartHooks. This allows us to show a more helpful message 55 // for duplicate registration. 56 originatingStack string 57 58 // done will be closed when the postHook is finished 59 done chan struct{} 60 } 61 62 type preShutdownHookEntry struct { 63 hook PreShutdownHookFunc 64 } 65 66 // AddPostStartHook allows you to add a PostStartHook. 67 func (s *GenericWebServer) AddPostStartHook(name string, hook PostStartHookFunc) error { 68 if len(name) == 0 { 69 return fmt.Errorf("missing name") 70 } 71 if hook == nil { 72 return fmt.Errorf("hook func may not be nil: %q", name) 73 } 74 75 s.postStartHookLock.Lock() 76 defer s.postStartHookLock.Unlock() 77 78 if s.postStartHooksCalled { 79 return fmt.Errorf("unable to add %q because PostStartHooks have already been called", name) 80 } 81 if postStartHook, exists := s.postStartHooks[name]; exists { 82 // this is programmer error, but it can be hard to debug 83 return fmt.Errorf( 84 "unable to add %q because it was already registered by: %s", 85 name, 86 postStartHook.originatingStack, 87 ) 88 } 89 90 // done is closed when the poststarthook is finished. This is used by the health check to be able to indicate 91 // that the poststarthook is finished 92 done := make(chan struct{}) 93 s.postStartHooks[name] = postStartHookEntry{hook: hook, originatingStack: string(debug.Stack()), done: done} 94 95 return nil 96 } 97 98 // AddPostStartHookOrDie allows you to add a PostStartHook, but dies on failure 99 func (s *GenericWebServer) AddPostStartHookOrDie(name string, hook PostStartHookFunc) { 100 if err := s.AddPostStartHook(name, hook); err != nil { 101 logrus.Fatalf("Error registering PostStartHook %q: %v", name, err) 102 } 103 } 104 105 // AddPreShutdownHook allows you to add a PreShutdownHook. 106 func (s *GenericWebServer) AddPreShutdownHook(name string, hook PreShutdownHookFunc) error { 107 if len(name) == 0 { 108 return fmt.Errorf("missing name") 109 } 110 if hook == nil { 111 return nil 112 } 113 114 s.preShutdownHookLock.Lock() 115 defer s.preShutdownHookLock.Unlock() 116 117 if s.preShutdownHooksCalled { 118 return fmt.Errorf("unable to add %q because PreShutdownHooks have already been called", name) 119 } 120 if _, exists := s.preShutdownHooks[name]; exists { 121 return fmt.Errorf("unable to add %q because it is already registered", name) 122 } 123 124 s.preShutdownHooks[name] = preShutdownHookEntry{hook: hook} 125 126 return nil 127 } 128 129 // AddPreShutdownHookOrDie allows you to add a PostStartHook, but dies on failure 130 func (s *GenericWebServer) AddPreShutdownHookOrDie(name string, hook PreShutdownHookFunc) { 131 if err := s.AddPreShutdownHook(name, hook); err != nil { 132 logrus.Fatalf("Error registering PreShutdownHook %q: %v", name, err) 133 } 134 } 135 136 // RunPostStartHooks runs the PostStartHooks for the server 137 func (s *GenericWebServer) RunPostStartHooks(ctx context.Context) { 138 s.postStartHookLock.Lock() 139 defer s.postStartHookLock.Unlock() 140 s.postStartHooksCalled = true 141 142 for hookName, hookEntry := range s.postStartHooks { 143 go runPostStartHook(ctx, hookName, hookEntry) 144 } 145 } 146 147 // RunPreShutdownHooks runs the PreShutdownHooks for the server 148 func (s *GenericWebServer) RunPreShutdownHooks() error { 149 var errorList []error 150 151 s.preShutdownHookLock.Lock() 152 defer s.preShutdownHookLock.Unlock() 153 s.preShutdownHooksCalled = true 154 155 for hookName, hookEntry := range s.preShutdownHooks { 156 if err := runPreShutdownHook(hookName, hookEntry); err != nil { 157 errorList = append(errorList, err) 158 } 159 } 160 return errors_.NewAggregate(errorList) 161 } 162 163 // isPostStartHookRegistered checks whether a given PostStartHook is registered 164 func (s *GenericWebServer) isPostStartHookRegistered(name string) bool { 165 s.postStartHookLock.Lock() 166 defer s.postStartHookLock.Unlock() 167 _, exists := s.postStartHooks[name] 168 return exists 169 } 170 171 func runPostStartHook(ctx context.Context, name string, entry postStartHookEntry) { 172 var err error 173 func() { 174 // don't let the hook *accidentally* panic and kill the server 175 //defer utilruntime.HandleCrash() 176 err = entry.hook(ctx) 177 }() 178 // if the hook intentionally wants to kill server, let it. 179 if err != nil { 180 logrus.Fatalf("PostStartHook %q failed: %v", name, err) 181 } 182 close(entry.done) 183 } 184 185 func runPreShutdownHook(name string, entry preShutdownHookEntry) error { 186 var err error 187 func() { 188 // don't let the hook *accidentally* panic and kill the server 189 // defer utilruntime.HandleCrash() 190 err = entry.hook() 191 }() 192 if err != nil { 193 return fmt.Errorf("PreShutdownHook %q failed: %v", name, err) 194 } 195 return nil 196 }