github.com/haraldrudell/parl@v0.4.176/ptime/on-ticker.go (about) 1 /* 2 © 2018–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package ptime 7 8 import ( 9 "context" 10 "sync" 11 "sync/atomic" 12 "time" 13 "unsafe" 14 15 "github.com/haraldrudell/parl/internal/cyclebreaker" 16 "github.com/haraldrudell/parl/perrors" 17 ) 18 19 // OnTicker is a ticker triggering on period-multiples since zero time 20 // - [time.Ticker] has a C field and Stop and Reset methods 21 // - onTickerThread is launched in the new function 22 // - — to avoid memory leaks, the thread cannot hold pointers to OnTicker 23 // - — but the thread and OnTicker can both point to shared data onTick 24 type OnTicker struct { 25 C <-chan time.Time // The channel on which the ticks are delivered. 26 onTickp *onTick 27 } 28 29 // onTick holds data shared between an OnTicker instance and its thread 30 type onTick struct { 31 period time.Duration 32 loc *time.Location 33 ticker *time.Ticker 34 isThreadExit atomic.Bool 35 wg sync.WaitGroup 36 cancel context.CancelFunc 37 ctx context.Context 38 } 39 40 // NewOnTicker returns a ticker that ticks on period-multiples since zero time. 41 // - loc is optional time zone, default time.Local, other time zone is time.UTC 42 // - — time zone matters for periods longer than 1 hour or time-zone offiset minutes 43 // - period must be greater than zero or panic 44 // - OnTicker is a time.Ticker enhanced with on-period multiples 45 func NewOnTicker(period time.Duration, loc ...*time.Location) (onTicker *OnTicker) { 46 var loc0 *time.Location 47 if len(loc) > 0 { 48 loc0 = loc[0] 49 } 50 if loc0 == nil { 51 loc0 = time.Local 52 } 53 // period panic in new function, not in thread 54 if period <= 0 { 55 panic(perrors.NewPF("period must be positive")) 56 } 57 58 // create onTick 59 var o = onTick{ 60 period: period, 61 loc: loc0, 62 ticker: time.NewTicker(time.Hour), 63 } 64 // ticker is a stopped ticker with empty channel 65 o.ticker.Stop() 66 o.ctx, o.cancel = context.WithCancel(context.Background()) 67 68 // launch thread 69 o.wg.Add(1) 70 go o.onTickerThread() 71 72 return &OnTicker{C: o.ticker.C, onTickp: &o} 73 } 74 75 func (o *OnTicker) Stop() { 76 o.onTickp.cancel() // signal to thread to cancel 77 o.onTickp.ticker.Stop() // ensure ticker is stopped 78 o.onTickp.wg.Wait() // wait for thread to exit 79 } 80 81 // onTickerThread aligns o.ticker with on-period, then exits 82 func (o *onTick) onTickerThread() { 83 defer o.isThreadExit.Store(true) 84 defer o.wg.Done() 85 var err error 86 defer cyclebreaker.Recover(func() cyclebreaker.DA { return cyclebreaker.A() }, &err, cyclebreaker.Infallible) 87 88 var timer = time.NewTimer(Duro(o.period, time.Now().In(o.loc))) 89 defer timer.Stop() 90 91 // - 1. wait for on-period time 92 // - 2. send a fake initial tick 93 // - 3. start ticker with correct period 94 // - 4. exit thread 95 done := o.ctx.Done() 96 Cin := timer.C 97 // convert read channel o.ticker.C to a send channel 98 Cout := *(*chan<- time.Time)(unsafe.Pointer(&o.ticker.C)) 99 var t time.Time 100 select { 101 case <-done: // context canceled 102 case t = <-Cin: 103 o.ticker.Reset(o.period) // start the ticker 104 Cout <- t // send fake initial tick 105 } 106 }