go.uber.org/yarpc@v1.72.1/dispatcher_startup.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 yarpc
    22  
    23  import (
    24  	"errors"
    25  	"sync"
    26  
    27  	"go.uber.org/yarpc/api/transport"
    28  	"go.uber.org/yarpc/internal/errorsync"
    29  
    30  	"go.uber.org/atomic"
    31  	"go.uber.org/multierr"
    32  	"go.uber.org/zap"
    33  )
    34  
    35  // PhasedStarter is a more granular alternative to the Dispatcher's all-in-one
    36  // Start method. Rather than starting the transports, inbounds, and outbounds
    37  // in one call, it lets the user choose when to trigger each phase of
    38  // dispatcher startup. For details on the interaction of Start and phased
    39  // startup, see the documentation for the Dispatcher's PhasedStart method.
    40  //
    41  // The user of a PhasedStarter is responsible for correctly ordering startup:
    42  // transports MUST be started before outbounds, which MUST be started before
    43  // inbounds. Attempting startup in any other order will return an error.
    44  type PhasedStarter struct {
    45  	startedMu sync.Mutex
    46  	started   []transport.Lifecycle
    47  
    48  	dispatcher *Dispatcher
    49  	log        *zap.Logger
    50  
    51  	transportsStartInitiated atomic.Bool
    52  	transportsStarted        atomic.Bool
    53  	outboundsStartInitiated  atomic.Bool
    54  	outboundsStarted         atomic.Bool
    55  	inboundsStartInitiated   atomic.Bool
    56  }
    57  
    58  // StartTransports is the first step in startup. It starts all transports
    59  // configured on the dispatcher, which is a necessary precondition for making
    60  // and receiving RPCs. It's safe to call concurrently, but all calls after the
    61  // first return an error.
    62  func (s *PhasedStarter) StartTransports() error {
    63  	if s.transportsStartInitiated.Swap(true) {
    64  		return errors.New("already began starting transports")
    65  	}
    66  	defer s.transportsStarted.Store(true)
    67  	s.log.Info("starting transports")
    68  	wait := errorsync.ErrorWaiter{}
    69  	for _, t := range s.dispatcher.transports {
    70  		wait.Submit(s.start(t))
    71  	}
    72  	if errs := wait.Wait(); len(errs) != 0 {
    73  		return s.abort(errs)
    74  	}
    75  	s.log.Debug("started transports")
    76  	return nil
    77  }
    78  
    79  // StartOutbounds is the second phase of startup. It starts all outbounds
    80  // configured on the dispatcher, which allows users of the dispatcher to
    81  // construct clients and begin making outbound RPCs. It's safe to call
    82  // concurrently, but all calls after the first return an error.
    83  func (s *PhasedStarter) StartOutbounds() error {
    84  	if !s.transportsStarted.Load() {
    85  		return errors.New("must start outbounds after transports")
    86  	}
    87  	if s.outboundsStartInitiated.Swap(true) {
    88  		return errors.New("already began starting outbounds")
    89  	}
    90  	defer s.outboundsStarted.Store(true)
    91  	s.log.Info("starting outbounds")
    92  	wait := errorsync.ErrorWaiter{}
    93  	for _, o := range s.dispatcher.outbounds {
    94  		wait.Submit(s.start(o.Unary))
    95  		wait.Submit(s.start(o.Oneway))
    96  		wait.Submit(s.start(o.Stream))
    97  	}
    98  	if errs := wait.Wait(); len(errs) != 0 {
    99  		return s.abort(errs)
   100  	}
   101  	s.log.Debug("started outbounds")
   102  	return nil
   103  }
   104  
   105  // StartInbounds is the final phase of startup. It starts all inbounds
   106  // configured on the dispatcher, which allows any registered procedures to
   107  // begin receiving requests. It's safe to call concurrently, but all calls
   108  // after the first return an error.
   109  func (s *PhasedStarter) StartInbounds() error {
   110  	if !s.transportsStarted.Load() || !s.outboundsStarted.Load() {
   111  		return errors.New("must start inbounds after transports and outbounds")
   112  	}
   113  	if s.inboundsStartInitiated.Swap(true) {
   114  		return errors.New("already began starting inbounds")
   115  	}
   116  	s.log.Info("starting inbounds")
   117  	wait := errorsync.ErrorWaiter{}
   118  	for _, i := range s.dispatcher.inbounds {
   119  		wait.Submit(s.start(i))
   120  	}
   121  	if errs := wait.Wait(); len(errs) != 0 {
   122  		return s.abort(errs)
   123  	}
   124  	s.log.Debug("started inbounds")
   125  	return nil
   126  }
   127  
   128  func (s *PhasedStarter) start(lc transport.Lifecycle) func() error {
   129  	return func() error {
   130  		if lc == nil {
   131  			return nil
   132  		}
   133  
   134  		if err := lc.Start(); err != nil {
   135  			return err
   136  		}
   137  
   138  		s.startedMu.Lock()
   139  		s.started = append(s.started, lc)
   140  		s.startedMu.Unlock()
   141  
   142  		return nil
   143  	}
   144  }
   145  
   146  func (s *PhasedStarter) abort(errs []error) error {
   147  	// Failed to start so stop everything that was started.
   148  	wait := errorsync.ErrorWaiter{}
   149  	s.startedMu.Lock()
   150  	for _, lc := range s.started {
   151  		wait.Submit(lc.Stop)
   152  	}
   153  	s.startedMu.Unlock()
   154  	if newErrors := wait.Wait(); len(newErrors) > 0 {
   155  		errs = append(errs, newErrors...)
   156  	}
   157  
   158  	return multierr.Combine(errs...)
   159  }
   160  
   161  func (s *PhasedStarter) setRouters() {
   162  	// Don't need synchronization, since we always call this in a lifecycle.Once
   163  	// in the dispatcher.
   164  	s.log.Debug("setting router for inbounds")
   165  	for _, ib := range s.dispatcher.inbounds {
   166  		ib.SetRouter(s.dispatcher.table)
   167  	}
   168  	s.log.Debug("set router for inbounds")
   169  }
   170  
   171  // PhasedStopper is a more granular alternative to the Dispatcher's all-in-one
   172  // Stop method. Rather than stopping the inbounds, outbounds, and transports
   173  // in one call, it lets the user choose when to trigger each phase of
   174  // dispatcher shutdown. For details on the interaction of Stop and phased
   175  // shutdown, see the documentation for the Dispatcher's PhasedStop method.
   176  //
   177  // The user of a PhasedStopper is responsible for correctly ordering shutdown:
   178  // inbounds MUST be stopped before outbounds, which MUST be stopped before
   179  // transports. Attempting shutdown in any other order will return an error.
   180  type PhasedStopper struct {
   181  	dispatcher *Dispatcher
   182  	log        *zap.Logger
   183  
   184  	inboundsStopInitiated   atomic.Bool
   185  	inboundsStopped         atomic.Bool
   186  	outboundsStopInitiated  atomic.Bool
   187  	outboundsStopped        atomic.Bool
   188  	transportsStopInitiated atomic.Bool
   189  }
   190  
   191  // StopInbounds is the first step in shutdown. It stops all inbounds
   192  // configured on the dispatcher, which stops routing RPCs to all registered
   193  // procedures. It's safe to call concurrently, but all calls after the first
   194  // return an error.
   195  func (s *PhasedStopper) StopInbounds() error {
   196  	if s.inboundsStopInitiated.Swap(true) {
   197  		return errors.New("already began stopping inbounds")
   198  	}
   199  	defer s.inboundsStopped.Store(true)
   200  	s.log.Debug("stopping inbounds")
   201  	wait := errorsync.ErrorWaiter{}
   202  	for _, ib := range s.dispatcher.inbounds {
   203  		wait.Submit(ib.Stop)
   204  	}
   205  	if errs := wait.Wait(); len(errs) > 0 {
   206  		return multierr.Combine(errs...)
   207  	}
   208  	s.log.Debug("stopped inbounds")
   209  	return nil
   210  }
   211  
   212  // StopOutbounds is the second step in shutdown. It stops all outbounds
   213  // configured on the dispatcher, which stops clients from making outbound
   214  // RPCs. It's safe to call concurrently, but all calls after the first return
   215  // an error.
   216  func (s *PhasedStopper) StopOutbounds() error {
   217  	if !s.inboundsStopped.Load() {
   218  		return errors.New("must stop inbounds first")
   219  	}
   220  	if s.outboundsStopInitiated.Swap(true) {
   221  		return errors.New("already began stopping outbounds")
   222  	}
   223  	defer s.outboundsStopped.Store(true)
   224  	s.log.Debug("stopping outbounds")
   225  	wait := errorsync.ErrorWaiter{}
   226  	for _, o := range s.dispatcher.outbounds {
   227  		if o.Unary != nil {
   228  			wait.Submit(o.Unary.Stop)
   229  		}
   230  		if o.Oneway != nil {
   231  			wait.Submit(o.Oneway.Stop)
   232  		}
   233  		if o.Stream != nil {
   234  			wait.Submit(o.Stream.Stop)
   235  		}
   236  	}
   237  	if errs := wait.Wait(); len(errs) > 0 {
   238  		return multierr.Combine(errs...)
   239  	}
   240  	s.log.Debug("stopped outbounds")
   241  	return nil
   242  }
   243  
   244  // StopTransports is the final step in shutdown. It stops all transports
   245  // configured on the dispatcher and cleans up any ancillary goroutines. It's
   246  // safe to call concurrently, but all calls after the first return an error.
   247  func (s *PhasedStopper) StopTransports() error {
   248  	if !s.inboundsStopped.Load() || !s.outboundsStopped.Load() {
   249  		return errors.New("must stop inbounds and outbounds first")
   250  	}
   251  	if s.transportsStopInitiated.Swap(true) {
   252  		return errors.New("already began stopping transports")
   253  	}
   254  	s.log.Debug("stopping transports")
   255  	wait := errorsync.ErrorWaiter{}
   256  	for _, t := range s.dispatcher.transports {
   257  		wait.Submit(t.Stop)
   258  	}
   259  	if errs := wait.Wait(); len(errs) > 0 {
   260  		return multierr.Combine(errs...)
   261  	}
   262  	s.log.Debug("stopped transports")
   263  
   264  	s.log.Debug("stopping metrics push loop, if any")
   265  	s.dispatcher.stopMeter()
   266  	s.log.Debug("stopped metrics push loop, if any")
   267  
   268  	return nil
   269  }