github.com/qioalice/ekago/v3@v3.3.2-0.20221202205325-5c262d586ee4/ekatime/once_in_private.go (about)

     1  // Copyright © 2021. All rights reserved.
     2  // Author: Ilya Stroy.
     3  // Contacts: iyuryevich@pm.me, https://github.com/qioalice
     4  // License: https://opensource.org/licenses/MIT
     5  
     6  package ekatime
     7  
     8  import (
     9  	"sync"
    10  	"sync/atomic"
    11  	"time"
    12  
    13  	"github.com/qioalice/ekago/v3/ekadeath"
    14  
    15  	heap "github.com/theodesp/go-heaps"
    16  	fibheap "github.com/theodesp/go-heaps/fibonacci"
    17  )
    18  
    19  /*
    20  "OnceIn" is a concept of repeatable delayed call of some functions when a time is come.
    21  It means you can say "execute function f each 1h" and it will be but starting with next hour.
    22  Your function won't be executed right now (unless otherwise specified).
    23  
    24  So, it like Golang's time.Ticker but you don't need to worry about channels
    25  for each time interval, about stopping/GC'ing timers/tickers, etc.
    26  
    27  All functions that you register are executed in one "worker" goroutine
    28  that is spawned when you calling some "OnceIn" function first time.
    29  If your function is heavy wrap it by your own runner that will start your function
    30  in a separate goroutine.
    31  
    32  It guarantees that ekadeath.Die() or ekadeath.Exit() calls won't shutdown your app
    33  when some your function is under executing, but next won't be executed.
    34  Even if they has the same time of firing.
    35  
    36  A minimum value of time you can use is a second. Time tolerance between
    37  "time has come" and "call your function in that time" is about 1 sec.
    38  */
    39  
    40  type (
    41  	// onceInUpdater is a special internal struct that gets the current timestamp
    42  	// once in some period and caching it allowing to get the cached data by getters.
    43  	onceInUpdater struct {
    44  
    45  		// WARNING!
    46  		// DO NOT CHANGE THE ORDER OF FIELDS!
    47  		// https://golang.org/pkg/sync/atomic/#pkg-note-BUG :
    48  		//
    49  		//   > On ARM, x86-32, and 32-bit MIPS,
    50  		//   > it is the caller's responsibility to arrange for 64-bit alignment
    51  		//   > of 64-bit words accessed atomically.
    52  		//   > The first word in a variable or in an allocated struct, array,
    53  		//   > or slice can be relied upon to be 64-bit aligned.
    54  		//
    55  		// Also:
    56  		// https://stackoverflow.com/questions/28670232/atomic-addint64-causes-invalid-memory-address-or-nil-pointer-dereference/51012703#51012703
    57  
    58  		/* 8b */ ts Timestamp // cached current Timestamp
    59  		/* 4b */ d Date // cached current Date
    60  		/* 4b */ t Time // cached current Time
    61  		/* 4b */ cbNum uint32 // number of associated callbacks
    62  		/* -- */ repeatDelay Timestamp
    63  	}
    64  
    65  	// onceInExeElem represents an execution element of "OnceIn" concept.
    66  	// It contains a function that should be called and a time as unix timestamp
    67  	// of when that function should be called.
    68  	onceInExeElem struct {
    69  		when        Timestamp
    70  		repeatDelay Timestamp
    71  		afterDelay  Timestamp
    72  		cb          OnceInCallback
    73  		cbPanic     OnceInPanicCallback
    74  		cbNum       uint32
    75  	}
    76  )
    77  
    78  //goland:noinspection GoSnakeCaseUsage
    79  const (
    80  	_ONCE_IN_SLEEP_TIME = 1 * time.Second
    81  )
    82  
    83  var (
    84  	// onceInFibHeap is a Fibonacci Heap of onceInExeElem's sorted to the nearest
    85  	// element that must be executed soon.
    86  	// Read more: https://en.wikipedia.org/wiki/Fibonacci_heap .
    87  	onceInFibHeap *fibheap.FibonacciHeap
    88  
    89  	// onceInFibHeapMu is a sync.Mutex that provides thread-safety for RW access
    90  	// to onceInFibHeap.
    91  	onceInFibHeapMu sync.Mutex
    92  
    93  	// onceInShutdownRequested is a "bool" atomic variable,
    94  	// that is set to 1 when shutdown is requested by ekadeath package.
    95  	onceInShutdownRequested int32
    96  
    97  	// onceInShutdownConfirmed is a channel, an ekadeath's destructor will wait a value from
    98  	// as a signal that it's safe to shutdown an app and no user's function
    99  	// is under execution right now.
   100  	// A worker guarantees that this channel will receive a value after
   101  	// onceInShutdownRequested is set to 1.
   102  	onceInShutdownConfirmed chan struct{}
   103  )
   104  
   105  // updateAll updates the cached data inside the current onceInUpdater
   106  // to the provided actual ones using atomic operations.
   107  func (oiu *onceInUpdater) updateAll(ts Timestamp, dd Date, t Time) {
   108  	atomic.StoreInt64((*int64)(&oiu.ts), int64(ts))
   109  	atomic.StoreUint32((*uint32)(&oiu.d), uint32(dd))
   110  	atomic.StoreUint32((*uint32)(&oiu.t), uint32(t))
   111  }
   112  
   113  func (oiu *onceInUpdater) update(ts Timestamp) {
   114  	dd, t := ts.Split()
   115  	oiu.updateAll(ts, dd, t)
   116  }
   117  
   118  // init calls update() and then register this using onceInFibHeap to be updated.
   119  func (oiu *onceInUpdater) init(now, repeatDelay Timestamp) {
   120  	// Init by 1, because 0 callback is the oiu.update (see last function's line).
   121  	oiu.cbNum = 1                 // 1 because of -----------------
   122  	oiu.update(now)               //                              |
   123  	oiu.repeatDelay = repeatDelay //                              v
   124  	onceInRegister(oiu.update, nil, repeatDelay, 0, false, false, 0)
   125  }
   126  
   127  // Compare implements `go-heaps.Item` interface.
   128  // It reports the comparing time difference of the current onceInExeElem and provided one.
   129  //
   130  // Returns:
   131  // -1 if current onceInExeElem's time < anotherElem's time,
   132  // 0 if they are the same,
   133  // 1 if current onceInExeElem's time > anotherElem's time.
   134  func (oie onceInExeElem) Compare(anotherOie heap.Item) int {
   135  	oie2 := anotherOie.(onceInExeElem)
   136  
   137  	if whenCmp := oie.when.Cmp(oie2.when); whenCmp != 0 {
   138  		return whenCmp
   139  
   140  	} else if repeatDelayCmp := oie.repeatDelay.Cmp(oie2.repeatDelay); repeatDelayCmp != 0 {
   141  		return repeatDelayCmp
   142  
   143  	} else if afterDelayCmp := oie.afterDelay.Cmp(oie2.afterDelay); afterDelayCmp != 0 {
   144  		return afterDelayCmp
   145  
   146  	} else {
   147  		return Timestamp(oie.cbNum).Cmp(Timestamp(oie2.cbNum))
   148  	}
   149  }
   150  
   151  // invoke invokes onceInExeElem's callback passing provided Timestamp,
   152  // checks whether it panics and if it so, calls onPanic callback.
   153  func (oie onceInExeElem) invoke(ts Timestamp) {
   154  	panicProtector := func(cb OnceInPanicCallback) {
   155  		if panicObj := recover(); panicObj != nil {
   156  			cb(panicObj)
   157  		}
   158  	}
   159  	if oie.cbPanic != nil {
   160  		defer panicProtector(oie.cbPanic)
   161  	}
   162  	oie.cb(ts)
   163  }
   164  
   165  // newOnceInExeElem is onceInExeElem constructor.
   166  func newOnceInExeElem(
   167  	repeatDelay, afterDelay Timestamp,
   168  	cb OnceInCallback, cbPanic OnceInPanicCallback, cbNum uint32,
   169  
   170  ) onceInExeElem {
   171  
   172  	return onceInExeElem{
   173  		when:        NewTimestampNow(),
   174  		repeatDelay: repeatDelay,
   175  		afterDelay:  afterDelay,
   176  		cb:          cb,
   177  		cbPanic:     cbPanic,
   178  		cbNum:       cbNum,
   179  	}
   180  }
   181  
   182  // onceInWorker is a special worker that is running in a background goroutine,
   183  // pulls nearest (by time) onceInExeElem from onceInFibHeap pool,
   184  // checks whether its time has come and if it so, executes a function.
   185  // Otherwise sleeps goroutine for _ONCE_IN_SLEEP_TIME duration.
   186  func onceInWorker() {
   187  
   188  	for atomic.LoadInt32(&onceInShutdownRequested) == 0 {
   189  		ts := NewTimestampNow()
   190  		onceInFibHeapMu.Lock()
   191  
   192  		nearestOie := onceInFibHeap.FindMin()
   193  		if nearestOie == nil || nearestOie.(onceInExeElem).when > ts {
   194  			// Pool of onceInExeElems is empty or nearest item's time not come yet.
   195  			// Abort current iteration, sleep, go next.
   196  			onceInFibHeapMu.Unlock()
   197  			time.Sleep(_ONCE_IN_SLEEP_TIME)
   198  			continue
   199  		}
   200  
   201  		// If we're here, nearestOie is not nil and its time has come.
   202  		_ = onceInFibHeap.DeleteMin() // the same as nearestOie
   203  
   204  		// Register next call.
   205  		nearestOieCopy := nearestOie.(onceInExeElem)
   206  		nearestOieCopy.when +=
   207  			nearestOieCopy.when.tillNext(nearestOieCopy.repeatDelay) +
   208  				nearestOieCopy.afterDelay
   209  		onceInFibHeap.Insert(nearestOieCopy)
   210  
   211  		onceInFibHeapMu.Unlock()
   212  
   213  		nearestOie.(onceInExeElem).invoke(ts)
   214  	}
   215  
   216  	// The loop above could be over only if shutdown is requested.
   217  	// So, if we're here, we need to confirm shutdown.
   218  	close(onceInShutdownConfirmed)
   219  }
   220  
   221  // onceInRegister registers a new OnceInCallback that must be called each `repeatDelay` time
   222  // waiting for `afterDelay` when time has come, calling `cb` and if it panics,
   223  // call then `panicCb[0]`. Calls right now if `invokeNow` is true.
   224  // Protects access to onceInFibHeap using associated sync.Mutex if `doLock` is true.
   225  func onceInRegister(
   226  	cb OnceInCallback, panicCb []OnceInPanicCallback,
   227  	repeatDelay, afterDelay Timestamp,
   228  	invokeNow, doLock bool,
   229  	cbNum uint32,
   230  ) {
   231  	if doLock {
   232  		onceInFibHeapMu.Lock()
   233  		defer onceInFibHeapMu.Unlock()
   234  	}
   235  
   236  	panicCb_ := OnceInPanicCallback(nil)
   237  	if len(panicCb) > 0 && panicCb[0] != nil {
   238  		panicCb_ = panicCb[0]
   239  	}
   240  
   241  	oie := newOnceInExeElem(repeatDelay, afterDelay, cb, panicCb_, cbNum)
   242  	if !invokeNow {
   243  		oie.when += oie.when.tillNext(repeatDelay) + afterDelay
   244  	}
   245  
   246  	onceInFibHeap.Insert(oie)
   247  }
   248  
   249  // initOnceIn initializes all package level onceInUpdater global variables,
   250  // registers onceInFibHeap destructor and starts its worker.
   251  func initOnceIn() {
   252  	onceInFibHeap = fibheap.New()
   253  	onceInShutdownConfirmed = make(chan struct{})
   254  
   255  	ekadeath.Reg(func() {
   256  		atomic.StoreInt32(&onceInShutdownRequested, 1)
   257  		<-onceInShutdownConfirmed
   258  	})
   259  
   260  	now := NewTimestampNow()
   261  
   262  	OnceInMinute.init(now, SECONDS_IN_MINUTE)
   263  	OnceIn10Minutes.init(now, SECONDS_IN_MINUTE*10)
   264  	OnceIn15Minutes.init(now, SECONDS_IN_MINUTE*15)
   265  	OnceIn30Minutes.init(now, SECONDS_IN_MINUTE*30)
   266  	OnceInHour.init(now, SECONDS_IN_HOUR)
   267  	OnceIn2Hour.init(now, SECONDS_IN_HOUR*2)
   268  	OnceIn3Hour.init(now, SECONDS_IN_HOUR*3)
   269  	OnceIn6Hour.init(now, SECONDS_IN_HOUR*6)
   270  	OnceIn12Hours.init(now, SECONDS_IN_HOUR*12)
   271  	OnceInDay.init(now, SECONDS_IN_DAY)
   272  
   273  	go onceInWorker()
   274  }