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  }