...
Tawesoft Logo

Source file src/tawesoft.co.uk/go/grace/grace.go

Documentation: src/tawesoft.co.uk/go/grace/grace.go

     1  package grace
     2  
     3  import (
     4      "context"
     5      "os"
     6      "os/signal"
     7      "sync"
     8      "syscall"
     9  )
    10  
    11  // Process defines a long-lived cancellable process
    12  type Process interface {
    13      // Start launches a process. This shouldn't block, so launch goroutines or
    14      // other programs as necessary. Call done() when that goroutine returns
    15      // or that process has terminated. If Start returns an error, then done()
    16      // must not be called.
    17      Start(done func()) error
    18  
    19      // Stop stops a process. Derive from the context as necessary e.g.
    20      // use context.WithTimeout
    21      Stop(context.Context) error
    22  }
    23  
    24  func shutdown(ctx context.Context, processes []Process) []error {
    25      var errors []error
    26  
    27      for _, process := range processes {
    28          if err := process.Stop(ctx); err != nil {
    29              errors = append(errors, err)
    30          }
    31      }
    32  
    33      return errors
    34  }
    35  
    36  // Run starts each process, waits for any signal in `notify` or
    37  // until ctx is cancelled, then cancels each process. It blocks until all
    38  // processes have been stopped with Stop() and all process Start() functions
    39  // mark themselves as completely done().
    40  //
    41  // The first return value is the os.Signal received.
    42  //
    43  // The second return value is a list of errors for processes that returned
    44  // errors when being cancelled.
    45  //
    46  // The third return value returns any startup error.
    47  //
    48  // The second return value may be non-empty even when the third return value is
    49  // non-nil when there is both a startup error and an error stopping any
    50  // previously started processes e.g. if process one starts but process two
    51  // fails, then process one needs to be cancelled but may also run into an error
    52  // cancelling.
    53  func Run(ctx context.Context, processes []Process, signals []os.Signal) (os.Signal, []error, error) {
    54  
    55      var wg sync.WaitGroup
    56      closechan := make(chan struct{sig os.Signal; errs []error}, 1)
    57  
    58      donefn := func() {
    59          wg.Done()
    60      }
    61  
    62      // start each process
    63      for i, process := range processes {
    64          err := process.Start(donefn)
    65          if err != nil {
    66              errors := shutdown(ctx, processes[0:i])
    67              wg.Wait()
    68              return syscall.Signal(0), errors, err
    69          }
    70          wg.Add(1)
    71      }
    72  
    73      // start the signal listener
    74      go func(ctx context.Context) {
    75          // block until interrupt signal, cancel signal, or context cancelled
    76          var result os.Signal
    77          sigchan := make(chan os.Signal, 1)
    78          signal.Notify(sigchan, signals...)
    79  
    80          select {
    81              case result = <- sigchan:
    82              case <- ctx.Done():
    83          }
    84  
    85          errors := shutdown(ctx, processes)
    86          wg.Wait()
    87          closechan <- struct{sig os.Signal; errs []error}{result, errors}
    88      }(ctx)
    89  
    90      // blocks until safe to exit
    91      result := <- closechan
    92      return result.sig, result.errs, nil
    93  }
    94  

View as plain text