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 }