github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/graceful/manager.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 package graceful 7 8 import ( 9 "context" 10 "runtime/pprof" 11 "sync" 12 "time" 13 14 "github.com/gitbundle/modules/log" 15 "github.com/gitbundle/modules/process" 16 "github.com/gitbundle/modules/setting" 17 ) 18 19 type state uint8 20 21 const ( 22 stateInit state = iota 23 stateRunning 24 stateShuttingDown 25 stateTerminate 26 ) 27 28 // There are three places that could inherit sockets: 29 // 30 // * HTTP or HTTPS main listener 31 // * HTTP redirection fallback 32 // * SSH 33 // 34 // If you add an additional place you must increment this number 35 // and add a function to call manager.InformCleanup if it's not going to be used 36 const numberOfServersToCreate = 4 37 38 // Manager represents the graceful server manager interface 39 var manager *Manager 40 41 var initOnce = sync.Once{} 42 43 // GetManager returns the Manager 44 func GetManager() *Manager { 45 InitManager(context.Background()) 46 return manager 47 } 48 49 // InitManager creates the graceful manager in the provided context 50 func InitManager(ctx context.Context) { 51 initOnce.Do(func() { 52 manager = newGracefulManager(ctx) 53 54 // Set the process default context to the HammerContext 55 process.DefaultContext = manager.HammerContext() 56 }) 57 } 58 59 // WithCallback is a runnable to call when the caller has finished 60 type WithCallback func(callback func()) 61 62 // RunnableWithShutdownFns is a runnable with functions to run at shutdown and terminate 63 // After the callback to atShutdown is called and is complete, the main function must return. 64 // Similarly the callback function provided to atTerminate must return once termination is complete. 65 // Please note that use of the atShutdown and atTerminate callbacks will create go-routines that will wait till their respective signals 66 // - users must therefore be careful to only call these as necessary. 67 type RunnableWithShutdownFns func(atShutdown, atTerminate func(func())) 68 69 // RunWithShutdownFns takes a function that has both atShutdown and atTerminate callbacks 70 // After the callback to atShutdown is called and is complete, the main function must return. 71 // Similarly the callback function provided to atTerminate must return once termination is complete. 72 // Please note that use of the atShutdown and atTerminate callbacks will create go-routines that will wait till their respective signals 73 // - users must therefore be careful to only call these as necessary. 74 func (g *Manager) RunWithShutdownFns(run RunnableWithShutdownFns) { 75 g.runningServerWaitGroup.Add(1) 76 defer g.runningServerWaitGroup.Done() 77 defer func() { 78 if err := recover(); err != nil { 79 log.Critical("PANIC during RunWithShutdownFns: %v\nStacktrace: %s", err, log.Stack(2)) 80 g.doShutdown() 81 } 82 }() 83 run(func(atShutdown func()) { 84 g.lock.Lock() 85 defer g.lock.Unlock() 86 g.toRunAtShutdown = append(g.toRunAtShutdown, 87 func() { 88 defer func() { 89 if err := recover(); err != nil { 90 log.Critical("PANIC during RunWithShutdownFns: %v\nStacktrace: %s", err, log.Stack(2)) 91 g.doShutdown() 92 } 93 }() 94 atShutdown() 95 }) 96 }, func(atTerminate func()) { 97 g.RunAtTerminate(atTerminate) 98 }) 99 } 100 101 // RunWithShutdownContext takes a function that has a context to watch for shutdown. 102 // After the provided context is Done(), the main function must return once shutdown is complete. 103 // (Optionally the HammerContext may be obtained and waited for however, this should be avoided if possible.) 104 func (g *Manager) RunWithShutdownContext(run func(context.Context)) { 105 g.runningServerWaitGroup.Add(1) 106 defer g.runningServerWaitGroup.Done() 107 defer func() { 108 if err := recover(); err != nil { 109 log.Critical("PANIC during RunWithShutdownContext: %v\nStacktrace: %s", err, log.Stack(2)) 110 g.doShutdown() 111 } 112 }() 113 ctx := g.ShutdownContext() 114 pprof.SetGoroutineLabels(ctx) // We don't have a label to restore back to but I think this is fine 115 run(ctx) 116 } 117 118 // RunAtTerminate adds to the terminate wait group and creates a go-routine to run the provided function at termination 119 func (g *Manager) RunAtTerminate(terminate func()) { 120 g.terminateWaitGroup.Add(1) 121 g.lock.Lock() 122 defer g.lock.Unlock() 123 g.toRunAtTerminate = append(g.toRunAtTerminate, 124 func() { 125 defer g.terminateWaitGroup.Done() 126 defer func() { 127 if err := recover(); err != nil { 128 log.Critical("PANIC during RunAtTerminate: %v\nStacktrace: %s", err, log.Stack(2)) 129 } 130 }() 131 terminate() 132 }) 133 } 134 135 // RunAtShutdown creates a go-routine to run the provided function at shutdown 136 func (g *Manager) RunAtShutdown(ctx context.Context, shutdown func()) { 137 g.lock.Lock() 138 defer g.lock.Unlock() 139 g.toRunAtShutdown = append(g.toRunAtShutdown, 140 func() { 141 defer func() { 142 if err := recover(); err != nil { 143 log.Critical("PANIC during RunAtShutdown: %v\nStacktrace: %s", err, log.Stack(2)) 144 } 145 }() 146 select { 147 case <-ctx.Done(): 148 return 149 default: 150 shutdown() 151 } 152 }) 153 } 154 155 // RunAtHammer creates a go-routine to run the provided function at shutdown 156 func (g *Manager) RunAtHammer(hammer func()) { 157 g.lock.Lock() 158 defer g.lock.Unlock() 159 g.toRunAtHammer = append(g.toRunAtHammer, 160 func() { 161 defer func() { 162 if err := recover(); err != nil { 163 log.Critical("PANIC during RunAtHammer: %v\nStacktrace: %s", err, log.Stack(2)) 164 } 165 }() 166 hammer() 167 }) 168 } 169 170 func (g *Manager) doShutdown() { 171 if !g.setStateTransition(stateRunning, stateShuttingDown) { 172 g.DoImmediateHammer() 173 return 174 } 175 g.lock.Lock() 176 g.shutdownCtxCancel() 177 atShutdownCtx := pprof.WithLabels(g.hammerCtx, pprof.Labels("graceful-lifecycle", "post-shutdown")) 178 pprof.SetGoroutineLabels(atShutdownCtx) 179 for _, fn := range g.toRunAtShutdown { 180 go fn() 181 } 182 g.lock.Unlock() 183 184 if setting.GracefulHammerTime >= 0 { 185 go g.doHammerTime(setting.GracefulHammerTime) 186 } 187 go func() { 188 g.WaitForServers() 189 // Mop up any remaining unclosed events. 190 g.doHammerTime(0) 191 <-time.After(1 * time.Second) 192 g.doTerminate() 193 g.WaitForTerminate() 194 g.lock.Lock() 195 g.managerCtxCancel() 196 g.lock.Unlock() 197 }() 198 } 199 200 func (g *Manager) doHammerTime(d time.Duration) { 201 time.Sleep(d) 202 g.lock.Lock() 203 select { 204 case <-g.hammerCtx.Done(): 205 default: 206 log.Warn("Setting Hammer condition") 207 g.hammerCtxCancel() 208 atHammerCtx := pprof.WithLabels(g.terminateCtx, pprof.Labels("graceful-lifecycle", "post-hammer")) 209 pprof.SetGoroutineLabels(atHammerCtx) 210 for _, fn := range g.toRunAtHammer { 211 go fn() 212 } 213 } 214 g.lock.Unlock() 215 } 216 217 func (g *Manager) doTerminate() { 218 if !g.setStateTransition(stateShuttingDown, stateTerminate) { 219 return 220 } 221 g.lock.Lock() 222 select { 223 case <-g.terminateCtx.Done(): 224 default: 225 log.Warn("Terminating") 226 g.terminateCtxCancel() 227 atTerminateCtx := pprof.WithLabels(g.managerCtx, pprof.Labels("graceful-lifecycle", "post-terminate")) 228 pprof.SetGoroutineLabels(atTerminateCtx) 229 230 for _, fn := range g.toRunAtTerminate { 231 go fn() 232 } 233 } 234 g.lock.Unlock() 235 } 236 237 // IsChild returns if the current process is a child of previous GitBundle process 238 func (g *Manager) IsChild() bool { 239 return g.isChild 240 } 241 242 // IsShutdown returns a channel which will be closed at shutdown. 243 // The order of closure is IsShutdown, IsHammer (potentially), IsTerminate 244 func (g *Manager) IsShutdown() <-chan struct{} { 245 return g.shutdownCtx.Done() 246 } 247 248 // IsHammer returns a channel which will be closed at hammer 249 // The order of closure is IsShutdown, IsHammer (potentially), IsTerminate 250 // Servers running within the running server wait group should respond to IsHammer 251 // if not shutdown already 252 func (g *Manager) IsHammer() <-chan struct{} { 253 return g.hammerCtx.Done() 254 } 255 256 // IsTerminate returns a channel which will be closed at terminate 257 // The order of closure is IsShutdown, IsHammer (potentially), IsTerminate 258 // IsTerminate will only close once all running servers have stopped 259 func (g *Manager) IsTerminate() <-chan struct{} { 260 return g.terminateCtx.Done() 261 } 262 263 // ServerDone declares a running server done and subtracts one from the 264 // running server wait group. Users probably do not want to call this 265 // and should use one of the RunWithShutdown* functions 266 func (g *Manager) ServerDone() { 267 g.runningServerWaitGroup.Done() 268 } 269 270 // WaitForServers waits for all running servers to finish. Users should probably 271 // instead use AtTerminate or IsTerminate 272 func (g *Manager) WaitForServers() { 273 g.runningServerWaitGroup.Wait() 274 } 275 276 // WaitForTerminate waits for all terminating actions to finish. 277 // Only the main go-routine should use this 278 func (g *Manager) WaitForTerminate() { 279 g.terminateWaitGroup.Wait() 280 } 281 282 func (g *Manager) getState() state { 283 g.lock.RLock() 284 defer g.lock.RUnlock() 285 return g.state 286 } 287 288 func (g *Manager) setStateTransition(old, new state) bool { 289 if old != g.getState() { 290 return false 291 } 292 g.lock.Lock() 293 if g.state != old { 294 g.lock.Unlock() 295 return false 296 } 297 g.state = new 298 g.lock.Unlock() 299 return true 300 } 301 302 func (g *Manager) setState(st state) { 303 g.lock.Lock() 304 defer g.lock.Unlock() 305 306 g.state = st 307 } 308 309 // InformCleanup tells the cleanup wait group that we have either taken a listener 310 // or will not be taking a listener 311 func (g *Manager) InformCleanup() { 312 g.createServerWaitGroup.Done() 313 } 314 315 // Done allows the manager to be viewed as a context.Context, it returns a channel that is closed when the server is finished terminating 316 func (g *Manager) Done() <-chan struct{} { 317 return g.managerCtx.Done() 318 } 319 320 // Err allows the manager to be viewed as a context.Context done at Terminate 321 func (g *Manager) Err() error { 322 return g.managerCtx.Err() 323 } 324 325 // Value allows the manager to be viewed as a context.Context done at Terminate 326 func (g *Manager) Value(key interface{}) interface{} { 327 return g.managerCtx.Value(key) 328 } 329 330 // Deadline returns nil as there is no fixed Deadline for the manager, it allows the manager to be viewed as a context.Context 331 func (g *Manager) Deadline() (deadline time.Time, ok bool) { 332 return g.managerCtx.Deadline() 333 }