github.com/haraldrudell/parl@v0.4.176/add-notifier.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  	"context"
    10  	"sync/atomic"
    11  )
    12  
    13  const (
    14  	// counts [parl.handleContextNotify] and [parl.invokeCancel]
    15  	cancelNotifierFrames = 2
    16  )
    17  
    18  // a NotifierFunc receives a stack trace of function causing cancel
    19  //   - typically stack trace begins with [parl.InvokeCancel]
    20  type NotifierFunc func(slice Stack)
    21  
    22  // notifier1Key is the context value key for child-context notifiers
    23  var notifier1Key cancelContextKey = "notifyChild"
    24  
    25  // notifierAllKey is the context value key for all-context notifiers
    26  var notifierAllKey cancelContextKey = "notifyAll"
    27  
    28  // threadSafeList is a thread-safe slice for all-cancel notifers
    29  type threadSafeList struct {
    30  	// - atomic.Pointer.Load for thread-safe read
    31  	// - CompareAndSwap of cloned list for thread-safe write
    32  	notifiers atomic.Pointer[[]NotifierFunc]
    33  }
    34  
    35  // AddNotifier1 adds a function that is invoked when a child context is canceled
    36  //   - child contexts with their own AddNotifier1 are not detected
    37  //   - invocation is immediately after context cancel completes
    38  //   - implemented by inserting a value into the context chain
    39  //   - notifier receives a stack trace of the cancel invocation,
    40  //     typically beginning with [parl.InvokeCancel]
    41  //   - notifier should be thread-safe and not long running
    42  //   - typical usage is debug of unexpected context cancel
    43  func AddNotifier1(ctx context.Context, notifier NotifierFunc) (ctx2 context.Context) {
    44  	if ctx == nil {
    45  		panic(NilError("ctx"))
    46  	} else if notifier == nil {
    47  		panic(NilError("notifier"))
    48  	}
    49  	return context.WithValue(ctx, notifier1Key, notifier)
    50  }
    51  
    52  // AddNotifier adds a function that is invoked when any context is canceled
    53  //   - AddNotifier is typically invoked on the root context
    54  //   - any InvokeCancel in the context tree below the top AddNotifier
    55  //     invocation causes notification
    56  //   - invocation is immediately after context cancel completes
    57  //   - implemented by inserting a thread-safe slice value into the context chain
    58  //   - notifier receives a stack trace of the cancel invocation,
    59  //     typically beginning with [parl.InvokeCancel]
    60  //   - notifier should be thread-safe and not long running
    61  //   - typical usage is debug of unexpected context cancel
    62  func AddNotifier(ctx context.Context, notifier NotifierFunc) (ctx2 context.Context) {
    63  	if ctx == nil {
    64  		panic(NilError("ctx"))
    65  	} else if notifier == nil {
    66  		panic(NilError("notifier"))
    67  	}
    68  
    69  	// if this context-chain has static notifier, append to it
    70  	if list, ok := ctx.Value(notifierAllKey).(*threadSafeList); ok { // ok only if non-nil
    71  		for {
    72  			// currentSlicep is read-only to be thread-safe
    73  			var currentSlicep = list.notifiers.Load()
    74  			// clone and append
    75  			var newSlice = append(append([]NotifierFunc{}, *currentSlicep...), notifier)
    76  			if list.notifiers.CompareAndSwap(currentSlicep, &newSlice) {
    77  				ctx2 = ctx // appended: ctx does not change
    78  				return     // append return
    79  			}
    80  		}
    81  	}
    82  
    83  	// create a new list
    84  	var newList threadSafeList
    85  	var newSlice = []NotifierFunc{notifier}
    86  	newList.notifiers.Store(&newSlice)
    87  
    88  	// insert list pointer into context chain
    89  	ctx2 = context.WithValue(ctx, notifierAllKey, &newList)
    90  	return // insert context value return, ctx2 new value
    91  }
    92  
    93  // handleContextNotify is invoked for all CancelContext cancel invocations
    94  func handleContextNotify(ctx context.Context) {
    95  	// fetch the nearest notify1 function
    96  	//	- notify1 are created by [parl.AddNotifier1] and are notified of
    97  	//		cancellation of a child context
    98  	var notifier, _ = ctx.Value(notifier1Key).(NotifierFunc)
    99  
   100  	// fetch any notifyall list
   101  	var notifiers []NotifierFunc
   102  	if list, ok := ctx.Value(notifierAllKey).(*threadSafeList); ok {
   103  		notifiers = *list.notifiers.Load()
   104  	}
   105  
   106  	if notifier == nil && len(notifiers) == 0 {
   107  		return // no notifiers return
   108  	}
   109  
   110  	// stack trace for notifiers: expensive
   111  	var cl = newStack(cancelNotifierFrames)
   112  
   113  	// invoke all notifier functions
   114  	if notifier != nil {
   115  		notifier(cl)
   116  	}
   117  	for _, n := range notifiers {
   118  		n(cl)
   119  	}
   120  }