go.uber.org/yarpc@v1.72.1/pkg/lifecycle/once.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package lifecycle
    22  
    23  import (
    24  	"context"
    25  	"errors"
    26  	syncatomic "sync/atomic"
    27  
    28  	"go.uber.org/atomic"
    29  	"go.uber.org/yarpc/yarpcerrors"
    30  )
    31  
    32  // State represents `states` that a lifecycle object can be in.
    33  type State int
    34  
    35  const (
    36  	// Idle indicates the Lifecycle hasn't been operated on yet.
    37  	Idle State = iota
    38  
    39  	// Starting indicates that the Lifecycle has begun it's "start" command
    40  	// but hasn't finished yet.
    41  	Starting
    42  
    43  	// Running indicates that the Lifecycle has finished starting and is
    44  	// available.
    45  	Running
    46  
    47  	// Stopping indicates that the Lifecycle 'stop' method has been called
    48  	// but hasn't finished yet.
    49  	Stopping
    50  
    51  	// Stopped indicates that the Lifecycle has been stopped.
    52  	Stopped
    53  
    54  	// Errored indicates that the Lifecycle experienced an error and we can't
    55  	// reasonably determine what state the lifecycle is in.
    56  	Errored
    57  )
    58  
    59  var stateToName = map[State]string{
    60  	Idle:     "idle",
    61  	Starting: "starting",
    62  	Running:  "running",
    63  	Stopping: "stopping",
    64  	Stopped:  "stopped",
    65  	Errored:  "errored",
    66  }
    67  
    68  func getStateName(s State) string {
    69  	if name, ok := stateToName[s]; ok {
    70  		return name
    71  	}
    72  	return "unknown"
    73  }
    74  
    75  // Once is a helper for implementing objects that advance monotonically through
    76  // lifecycle states using at-most-once start and stop implementations in a
    77  // thread safe manner.
    78  type Once struct {
    79  	// startCh closes to allow goroutines to resume after the lifecycle is in
    80  	// the Running state or beyond.
    81  	startCh chan struct{}
    82  	// stoppingCh closes to allow goroutines to resume after the lifecycle is
    83  	// in the Stopping state or beyond.
    84  	stoppingCh chan struct{}
    85  	// stopCh closes to allow goroutines to resume after the lifecycle is in
    86  	// the Stopped state or Errored.
    87  	stopCh chan struct{}
    88  	// err is the error, if any, that Start() or Stop() returned and all
    89  	// subsequent Start() or Stop() calls will return. The right to set
    90  	// err is conferred to whichever goroutine is starting or stopping, until
    91  	// it has started or stopped, after which `err` becomes immutable.
    92  	err syncatomic.Value
    93  	// state is an atomic State representing the object's current
    94  	// state (Idle, Starting, Running, Stopping, Stopped, Errored).
    95  	state atomic.Int32
    96  }
    97  
    98  // NewOnce returns a lifecycle controller.
    99  //
   100  //   0. The observable lifecycle state must only go forward from birth to death.
   101  //   1. Start() must block until the state is >= Running
   102  //   2. Stop() must block until the state is >= Stopped
   103  //   3. Stop() must pre-empt Start() if it occurs first
   104  //   4. Start() and Stop() may be backed by a do-actual-work function, and that
   105  //      function must be called at-most-once.
   106  func NewOnce() *Once {
   107  	return &Once{
   108  		startCh:    make(chan struct{}),
   109  		stoppingCh: make(chan struct{}),
   110  		stopCh:     make(chan struct{}),
   111  	}
   112  }
   113  
   114  // Start will run the `f` function once and return the error.
   115  // If Start is called multiple times it will return the error
   116  // from the first time it was called.
   117  func (o *Once) Start(f func() error) error {
   118  	if o.state.CAS(int32(Idle), int32(Starting)) {
   119  		var err error
   120  		if f != nil {
   121  			err = f()
   122  		}
   123  
   124  		// skip forward to error state
   125  		if err != nil {
   126  			o.setError(err)
   127  			o.state.Store(int32(Errored))
   128  			close(o.stoppingCh)
   129  			close(o.stopCh)
   130  		} else {
   131  			o.state.Store(int32(Running))
   132  		}
   133  		close(o.startCh)
   134  
   135  		return err
   136  	}
   137  
   138  	<-o.startCh
   139  	return o.loadError()
   140  }
   141  
   142  // WaitUntilRunning blocks until the instance enters the running state, or the
   143  // context times out.
   144  func (o *Once) WaitUntilRunning(ctx context.Context) error {
   145  	state := State(o.state.Load())
   146  	if state == Running {
   147  		return nil
   148  	}
   149  	if state > Running {
   150  		return yarpcerrors.FailedPreconditionErrorf("could not wait for instance to start running: current state is %q", getStateName(state))
   151  	}
   152  
   153  	if _, ok := ctx.Deadline(); !ok {
   154  		return yarpcerrors.InvalidArgumentErrorf("could not wait for instance to start running: deadline required on request context")
   155  	}
   156  
   157  	select {
   158  	case <-o.startCh:
   159  		state := State(o.state.Load())
   160  		if state == Running {
   161  			return nil
   162  		}
   163  		return yarpcerrors.FailedPreconditionErrorf("instance did not enter running state, current state is %q", getStateName(state))
   164  	case <-ctx.Done():
   165  		return yarpcerrors.FailedPreconditionErrorf("context finished while waiting for instance to start: %s", ctx.Err().Error())
   166  	}
   167  }
   168  
   169  // Stop will run the `f` function once and return the error.
   170  // If Stop is called multiple times it will return the error
   171  // from the first time it was called.
   172  func (o *Once) Stop(f func() error) error {
   173  	if o.state.CAS(int32(Idle), int32(Stopped)) {
   174  		close(o.startCh)
   175  		close(o.stoppingCh)
   176  		close(o.stopCh)
   177  		return nil
   178  	}
   179  
   180  	<-o.startCh
   181  
   182  	if o.state.CAS(int32(Running), int32(Stopping)) {
   183  		close(o.stoppingCh)
   184  
   185  		var err error
   186  		if f != nil {
   187  			err = f()
   188  		}
   189  
   190  		if err != nil {
   191  			o.setError(err)
   192  			o.state.Store(int32(Errored))
   193  		} else {
   194  			o.state.Store(int32(Stopped))
   195  		}
   196  		close(o.stopCh)
   197  		return err
   198  	}
   199  
   200  	<-o.stopCh
   201  	return o.loadError()
   202  }
   203  
   204  // Started returns a channel that will close when the lifecycle starts.
   205  func (o *Once) Started() <-chan struct{} {
   206  	return o.startCh
   207  }
   208  
   209  // Stopping returns a channel that will close when the lifecycle is stopping.
   210  func (o *Once) Stopping() <-chan struct{} {
   211  	return o.stoppingCh
   212  }
   213  
   214  // Stopped returns a channel that will close when the lifecycle stops.
   215  func (o *Once) Stopped() <-chan struct{} {
   216  	return o.stopCh
   217  }
   218  
   219  func (o *Once) setError(err error) {
   220  	o.err.Store(err)
   221  }
   222  
   223  func (o *Once) loadError() error {
   224  	errVal := o.err.Load()
   225  	if errVal == nil {
   226  		return nil
   227  	}
   228  
   229  	if err, ok := errVal.(error); ok {
   230  		return err
   231  	}
   232  
   233  	// TODO replace with DPanic log
   234  	return errors.New("lifecycle err was not `error` type")
   235  }
   236  
   237  // State returns the state of the object within its life cycle, from
   238  // start to full stop.
   239  // The function only guarantees that the lifecycle has at least passed through
   240  // the returned state and may have progressed further in the intervening time.
   241  func (o *Once) State() State {
   242  	return State(o.state.Load())
   243  }
   244  
   245  // IsRunning will return true if current state of the Lifecycle is running
   246  func (o *Once) IsRunning() bool {
   247  	return o.State() == Running
   248  }