go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/async/latch.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package async 9 10 import ( 11 "errors" 12 "sync" 13 "sync/atomic" 14 ) 15 16 // NewLatch creates a new latch. 17 // 18 // It is _highly_ recommended to use this constructor. 19 func NewLatch() *Latch { 20 l := new(Latch) 21 l.Reset() 22 return l 23 } 24 25 // Latch states 26 // 27 // They are int32 to support atomic Load/Store calls. 28 const ( 29 LatchStopped int32 = 0 30 LatchStarting int32 = 1 31 LatchResuming int32 = 2 32 LatchStarted int32 = 3 33 LatchActive int32 = 4 34 LatchPausing int32 = 5 35 LatchPaused int32 = 6 36 LatchStopping int32 = 7 37 ) 38 39 // Errors 40 var ( 41 ErrCannotStart = errors.New("cannot start; already started") 42 ErrCannotStop = errors.New("cannot stop; already stopped") 43 ErrCannotCancel = errors.New("cannot cancel; already canceled") 44 ErrCannotStartActionRequired = errors.New("cannot start; action is required") 45 ) 46 47 /* 48 Latch is a helper to coordinate goroutine lifecycles, specifically waiting for goroutines to start and end. 49 50 The lifecycle is generally as follows: 51 52 0 - stopped - goto 1 53 1 - starting - goto 2 54 2 - started - goto 3 55 3 - stopping - goto 0 56 57 Control flow is coordinated with chan struct{}, which acts as a semaphore but can only 58 alert (1) listener as it is buffered. 59 60 In order to start a `stopped` latch, you must call `.Reset()` first to initialize channels. 61 */ 62 type Latch struct { 63 sync.Mutex 64 65 state int32 66 67 starting chan struct{} 68 started chan struct{} 69 stopping chan struct{} 70 stopped chan struct{} 71 } 72 73 // Reset resets the latch. 74 func (l *Latch) Reset() { 75 l.Lock() 76 atomic.StoreInt32(&l.state, LatchStopped) 77 l.starting = make(chan struct{}, 1) 78 l.started = make(chan struct{}, 1) 79 l.stopping = make(chan struct{}, 1) 80 l.stopped = make(chan struct{}, 1) 81 l.Unlock() 82 } 83 84 // CanStart returns if the latch can start. 85 func (l *Latch) CanStart() bool { 86 return atomic.LoadInt32(&l.state) == LatchStopped 87 } 88 89 // CanStop returns if the latch can stop. 90 func (l *Latch) CanStop() bool { 91 return atomic.LoadInt32(&l.state) == LatchStarted 92 } 93 94 // IsStarting returns if the latch state is LatchStarting 95 func (l *Latch) IsStarting() bool { 96 return atomic.LoadInt32(&l.state) == LatchStarting 97 } 98 99 // IsStarted returns if the latch state is LatchStarted. 100 func (l *Latch) IsStarted() bool { 101 return atomic.LoadInt32(&l.state) == LatchStarted 102 } 103 104 // IsStopping returns if the latch state is LatchStopping. 105 func (l *Latch) IsStopping() bool { 106 return atomic.LoadInt32(&l.state) == LatchStopping 107 } 108 109 // IsStopped returns if the latch state is LatchStopped. 110 func (l *Latch) IsStopped() bool { 111 return atomic.LoadInt32(&l.state) == LatchStopped 112 } 113 114 // NotifyStarting returns the starting signal. 115 // It is used to coordinate the transition from stopped -> starting. 116 // There can only be (1) effective listener at a time for these events. 117 func (l *Latch) NotifyStarting() (notifyStarting <-chan struct{}) { 118 l.Lock() 119 notifyStarting = l.starting 120 l.Unlock() 121 return 122 } 123 124 // NotifyStarted returns the started signal. 125 // It is used to coordinate the transition from starting -> started. 126 // There can only be (1) effective listener at a time for these events. 127 func (l *Latch) NotifyStarted() (notifyStarted <-chan struct{}) { 128 l.Lock() 129 notifyStarted = l.started 130 l.Unlock() 131 return 132 } 133 134 // NotifyStopping returns the should stop signal. 135 // It is used to trigger the transition from running -> stopping -> stopped. 136 // There can only be (1) effective listener at a time for these events. 137 func (l *Latch) NotifyStopping() (notifyStopping <-chan struct{}) { 138 l.Lock() 139 notifyStopping = l.stopping 140 l.Unlock() 141 return 142 } 143 144 // NotifyStopped returns the stopped signal. 145 // It is used to coordinate the transition from stopping -> stopped. 146 // There can only be (1) effective listener at a time for these events. 147 func (l *Latch) NotifyStopped() (notifyStopped <-chan struct{}) { 148 l.Lock() 149 notifyStopped = l.stopped 150 l.Unlock() 151 return 152 } 153 154 // Starting signals the latch is starting. 155 // This is typically done before you kick off a goroutine. 156 func (l *Latch) Starting() { 157 if l.IsStarting() { 158 return 159 } 160 atomic.StoreInt32(&l.state, LatchStarting) 161 l.starting <- struct{}{} 162 } 163 164 // Started signals that the latch is started and has entered 165 // the `IsStarted` state. 166 func (l *Latch) Started() { 167 if l.IsStarted() { 168 return 169 } 170 atomic.StoreInt32(&l.state, LatchStarted) 171 l.started <- struct{}{} 172 } 173 174 // Stopping signals the latch to stop. 175 // It could also be thought of as `SignalStopping`. 176 func (l *Latch) Stopping() { 177 if l.IsStopping() { 178 return 179 } 180 atomic.StoreInt32(&l.state, LatchStopping) 181 l.stopping <- struct{}{} 182 } 183 184 // Stopped signals the latch has stopped. 185 func (l *Latch) Stopped() { 186 if l.IsStopped() { 187 return 188 } 189 atomic.StoreInt32(&l.state, LatchStopped) 190 l.stopped <- struct{}{} 191 } 192 193 // WaitStarted triggers `Starting` and waits for the `Started` signal. 194 func (l *Latch) WaitStarted() { 195 if !l.CanStart() { 196 return 197 } 198 started := l.NotifyStarted() 199 l.Starting() 200 <-started 201 } 202 203 // WaitStopped triggers `Stopping` and waits for the `Stopped` signal. 204 func (l *Latch) WaitStopped() { 205 if !l.CanStop() { 206 return 207 } 208 stopped := l.NotifyStopped() 209 l.Stopping() 210 <-stopped 211 }