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 }