go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/async/interval.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 "context" 12 "sync" 13 "time" 14 ) 15 16 /* 17 NewInterval returns a new worker that runs an action on an interval. 18 19 Example: 20 21 iw := &Interval{ 22 Action: func(ctx context.Context) error { 23 fmt.Println("running!") 24 return nil 25 }, 26 Interval: 500*time.Millisecond, 27 } 28 go iw.Start() 29 <-iw.Started() 30 31 */ 32 33 // Interval is a background worker that performs an action on an interval. 34 type Interval struct { 35 Context context.Context 36 Interval time.Duration 37 Action func(context.Context) error 38 Delay time.Duration 39 StopOnError bool 40 Errors chan error 41 42 latchMu sync.Mutex 43 latch *Latch 44 } 45 46 // Interval defaults 47 const ( 48 DefaultInterval = 500 * time.Millisecond 49 ) 50 51 // Background returns the provided context or context.Background() 52 func (i *Interval) Background() context.Context { 53 if i.Context != nil { 54 return i.Context 55 } 56 return context.Background() 57 } 58 59 // IntervalOrDefault returns the interval or a default. 60 func (i *Interval) IntervalOrDefault() time.Duration { 61 if i.Interval > 0 { 62 return i.Interval 63 } 64 return DefaultInterval 65 } 66 67 // Started returns the channel to notify when the worker starts. 68 func (i *Interval) Started() <-chan struct{} { 69 return i.latch.NotifyStarted() 70 } 71 72 /* 73 Start starts the worker. 74 75 This will start the internal ticker, with a default initial delay of the given interval, and will return an ErrCannotStart if the interval worker is already started. 76 77 This call will block. 78 */ 79 func (i *Interval) Start() error { 80 i.ensureLatch() 81 if !i.latch.CanStart() { 82 return ErrCannotStart 83 } 84 if i.Action == nil { 85 return ErrCannotStartActionRequired 86 } 87 i.latch.Starting() 88 return i.Dispatch() 89 } 90 91 // Stop stops the worker. 92 func (i *Interval) Stop() error { 93 if !i.latch.CanStop() { 94 return ErrCannotStop 95 } 96 i.latch.Stopping() 97 <-i.latch.NotifyStopped() 98 i.latch.Reset() // reset the latch in case we have to start again 99 return nil 100 } 101 102 // Dispatch is the main dispatch loop. 103 func (i *Interval) Dispatch() (err error) { 104 i.latch.Started() 105 106 if i.Delay > 0 { 107 alarm := time.NewTimer(i.Delay) 108 stopping := i.latch.NotifyStopping() 109 select { 110 case <-i.Context.Done(): 111 alarm.Stop() 112 return 113 case <-stopping: 114 alarm.Stop() 115 return 116 case <-alarm.C: 117 alarm.Stop() 118 } 119 } 120 121 tick := time.NewTicker(i.IntervalOrDefault()) 122 defer func() { 123 tick.Stop() 124 i.latch.Stopped() 125 }() 126 127 var stopping <-chan struct{} 128 for { 129 stopping = i.latch.NotifyStopping() 130 select { 131 case <-i.Context.Done(): 132 return 133 case <-stopping: 134 return 135 default: 136 } 137 138 select { 139 case <-i.Context.Done(): 140 return 141 case <-stopping: 142 return 143 case <-tick.C: 144 err = i.Action(i.Background()) 145 if err != nil { 146 if i.StopOnError { 147 return 148 } 149 if i.Errors != nil { 150 select { 151 case <-stopping: 152 return 153 case <-i.Context.Done(): 154 return 155 case i.Errors <- err: 156 } 157 } 158 } 159 } 160 } 161 } 162 163 func (i *Interval) ensureLatch() { 164 i.latchMu.Lock() 165 defer i.latchMu.Unlock() 166 if i.latch == nil { 167 i.latch = NewLatch() 168 } 169 }