github.com/haraldrudell/parl@v0.4.176/invocation-timer.go (about) 1 /* 2 © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package parl 7 8 import ( 9 "sync" 10 "sync/atomic" 11 "time" 12 13 "github.com/haraldrudell/parl/ptime" 14 ) 15 16 const ( 17 // timer thread checks for hung invocations every 10 seconds 18 // - minimum value, can be set to longer 19 defaultTimer = 10 * time.Second 20 ) 21 22 // CBFunc is a thread-safe function invoked on 23 // - reason == ITParallelism: parallelism exceeding parallelismWarningPoint 24 // - reason == ITLatency: latency of an ongoing or just ended invocation 25 // exceeds latencyWarningPoint 26 type CBFunc func(reason CBReason, maxParallelism uint64, maxLatency time.Duration, threadID ThreadID) 27 28 // InvocationTimer monitors funtion invocations for parallelism and latency 29 // - callback is invoked on exceeding thresholds and reaching a new max 30 // - runs one thread per instance while an invocation is active 31 type InvocationTimer[T any] struct { 32 callback CBFunc 33 endCb func(T) 34 timerPeriod time.Duration 35 goGen GoGen 36 37 pointerLock sync.Mutex 38 // pointer to linked list of Invocations 39 // - head is read to find oldest current invocation 40 // - head is written to insert, update or delete oldest invocation 41 // - tail is used to insert, update or delete newest invocation 42 head atomic.Pointer[Invocation[T]] // written behind pointerlock 43 tail *Invocation[T] // behind pointerLock 44 45 invos AtomicCounter 46 latency AtomicMax[time.Duration] 47 parallelism AtomicMax[uint64] 48 49 threadLock sync.Mutex 50 subGo SubGo // behind threadLock 51 } 52 53 // NewInvocationTimer returns an object alerting of max latency and parallelism 54 // - Do is used for new invocations 55 func NewInvocationTimer[T any]( 56 callback CBFunc, endCb func(T), 57 latencyWarningPoint time.Duration, parallelismWarningPoint uint64, 58 timerPeriod time.Duration, 59 goGen GoGen, 60 ) (invokeTimer *InvocationTimer[T]) { 61 if callback == nil { 62 panic(NilError("callback")) 63 } 64 if timerPeriod < defaultTimer { 65 timerPeriod = defaultTimer 66 } 67 return &InvocationTimer[T]{ 68 callback: callback, 69 endCb: endCb, 70 timerPeriod: timerPeriod, 71 goGen: goGen, 72 latency: *NewAtomicMax(latencyWarningPoint), 73 parallelism: *NewAtomicMax(parallelismWarningPoint), 74 } 75 } 76 77 // Oldest returns the oldest invocation 78 // - threadID is ID of oldest active thread, if any 79 // - age is longest ever invocation 80 // - if no invocation is active, age is 0, threadID invalid 81 func (i *InvocationTimer[T]) Oldest() (age time.Duration, threadID ThreadID) { 82 83 // get any active invocation 84 var invocation = i.head.Load() 85 if invocation == nil { 86 return // no active invocation return 87 } 88 threadID = invocation.ThreadID 89 90 // get age of oldest active invocation 91 age = invocation.Age() 92 if age2, _ := i.latency.Max(); age2 > age { 93 age = age2 94 } 95 96 return 97 } 98 99 // Invocation registers a new invocation with callbacks for parallelism and latency 100 // - caller invokes deferFunc at end of invocation 101 // 102 // Usage: 103 // 104 // func someFunc() { 105 // defer invocationTimer.Invocation()() 106 func (i *InvocationTimer[T]) Invocation(value T) (deferFunc func()) { 107 var invocation = NewInvocation(i.invocationEnd, value) // one allocation 108 i.insert(invocation) 109 i.ensureTimer() 110 var invos = i.invos.Value() 111 if i.parallelism.Value(invos) { 112 var max, _ = i.latency.Max() 113 // callback for high parallelism warning 114 i.callback(ITParallelism, invos, max, invocation.ThreadID) 115 } 116 return invocation.DeferFunc // one allocation 117 } 118 119 // invocationEnd is invoked by the Invocation instance’s deferred function 120 func (i *InvocationTimer[T]) invocationEnd(invocation *Invocation[T], duration time.Duration) { 121 i.remove(invocation) 122 i.maybeCancelTimer() 123 if i.latency.Value(duration) { 124 var max, _ = i.parallelism.Max() 125 // callback for slowness of completed task 126 i.callback(ITLatency, max, duration, invocation.ThreadID) 127 } 128 if cb := i.endCb; cb != nil { 129 cb(invocation.Value) 130 } 131 } 132 133 func (i *InvocationTimer[T]) insert(invocation *Invocation[T]) { 134 i.pointerLock.Lock() 135 defer i.pointerLock.Unlock() 136 137 // link in at tail 138 var tail = i.tail 139 if tail != nil { 140 invocation.Prev.Store(tail) 141 tail.Next.Store(invocation) 142 } 143 i.tail = invocation 144 145 // if first item, update head 146 if tail == nil { 147 i.head.Store(invocation) 148 } 149 } 150 151 func (i *InvocationTimer[T]) remove(invocation *Invocation[T]) { 152 i.pointerLock.Lock() 153 defer i.pointerLock.Unlock() 154 155 var prev = invocation.Prev.Load() 156 var next = invocation.Next.Load() 157 158 // unlink at previous item 159 if prev == nil { 160 i.head.Store(next) 161 } else { 162 prev.Next.Store(next) 163 } 164 165 // unlink at next item 166 if next == nil { 167 i.tail = prev 168 } else { 169 next.Prev.Store(prev) 170 } 171 } 172 173 // ensureTimer ensures that a time is eventually running if it should be 174 func (i *InvocationTimer[T]) ensureTimer() { 175 // if this was not the first invocation from idle, 176 // a thread does not have to be launched 177 if i.invos.Inc() != 1 { 178 return // this was not the initial invocation 179 } 180 181 i.threadLock.Lock() 182 defer i.threadLock.Unlock() 183 184 if i.invos.Value() == 0 { 185 return // other threads decremented value to zero 186 } else if i.subGo != nil { 187 return // some other thread already launched the timer thread 188 } 189 190 // order thread launch 191 var subGo = i.goGen.SubGo() 192 i.subGo = subGo 193 go i.hungInvocationCheckThread(ptime.NewOnTicker(i.timerPeriod, time.Local), subGo.Go()) 194 } 195 196 // maybeCancelTimer ensures that any timer thread ordered to launch will exit 197 func (i *InvocationTimer[T]) maybeCancelTimer() { 198 // if the number iof invocations does not go to zero, 199 // a thread does not need to be stopped 200 if i.invos.Dec() != 0 { 201 return // more invocations are active 202 } 203 204 i.threadLock.Lock() 205 defer i.threadLock.Unlock() 206 207 if i.invos.Value() != 0 { 208 return // another thread launched invocations 209 } 210 211 // cancel timer thread 212 var subGo = i.subGo 213 if subGo == nil { 214 return // another thread already shut down the timer thread 215 } 216 i.subGo = nil 217 subGo.Cancel() 218 } 219 220 // hungInvocationCheckThread looks for invocations that do not return 221 func (i *InvocationTimer[T]) hungInvocationCheckThread(ticker *ptime.OnTicker, g Go) { 222 var err error 223 defer g.Register().Done(&err) 224 defer RecoverErr(func() DA { return A() }, &err) 225 226 C := ticker.C 227 done := g.Context().Done() 228 for { 229 select { 230 case <-done: 231 return 232 case <-C: 233 } 234 235 // get oldest active invocation 236 var oldestInvocation = i.head.Load() 237 if oldestInvocation == nil { 238 continue // noop: no invocation 239 } 240 241 // check if it is oldest yet 242 var age = oldestInvocation.Age() 243 if !i.latency.Value(age) { 244 continue // not oldest yet 245 } 246 247 // invoke callback 248 var max, _ = i.parallelism.Max() 249 // callback for high latency of task in progress 250 i.callback(ITLatency, max, age, oldestInvocation.ThreadID) 251 } 252 }