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  }