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 }