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  }