github.com/haraldrudell/parl@v0.4.176/cancel-context.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  	"errors"
    11  
    12  	"github.com/haraldrudell/parl/perrors"
    13  )
    14  
    15  // ErrNotCancelContext indicates that InvokeCancel was provided a context
    16  // not a return value from NewCancelContext or NewCancelContextFunc.
    17  //
    18  // Test for ErrNotCancelContext:
    19  //
    20  //	if errors.Is(err, parl.ErrNotCancelContext) …
    21  var ErrNotCancelContext = errors.New("context chain does not have CancelContext")
    22  
    23  // cancelContextKey is a unique named type for storing and retrieving cancelFunc
    24  //   - used with [context.WithValue]
    25  type cancelContextKey string
    26  
    27  // cancelKey is a unique value for storing and retrieving cancelFunc
    28  //   - used with [context.WithValue]
    29  var cancelKey cancelContextKey = "parl.NewCancelContext"
    30  
    31  // NewCancelContext creates a cancelable context without managing a CancelFunction value
    32  //   - NewCancelContext is like [context.WithCancel] but has the CancelFunc embedded.
    33  //   - after use, [InvokeCancel] must be invoked with cancelCtx as argument to
    34  //     release resources
    35  //   - —
    36  //   - for unexported code in context package to work, a separate type cannot be used
    37  //
    38  // Usage:
    39  //
    40  //	ctx := NewCancelContext(context.Background())
    41  //	…
    42  //	InvokeCancel(ctx)
    43  func NewCancelContext(ctx context.Context) (cancelCtx context.Context) {
    44  	return NewCancelContextFunc(context.WithCancel(ctx))
    45  }
    46  
    47  // NewCancelContextFunc stores the cancel function cancel in the context ctx.
    48  // the returned context can be provided to InvokeCancel to cancel the context.
    49  func NewCancelContextFunc(ctx context.Context, cancel context.CancelFunc) (cancelCtx context.Context) {
    50  	return context.WithValue(ctx, cancelKey, cancel)
    51  }
    52  
    53  // HasCancel return if ctx can be used with [parl.InvokeCancel]
    54  //   - such contexts are returned by [parl.NewCancelContext]
    55  func HasCancel(ctx context.Context) (hasCancel bool) {
    56  	if ctx == nil {
    57  		return
    58  	}
    59  	cancel, _ := ctx.Value(cancelKey).(context.CancelFunc)
    60  	return cancel != nil
    61  }
    62  
    63  // InvokeCancel cancels the last CancelContext in ctx’ chain of contexts
    64  //   - ctx must have been returned by either NewCancelContext or NewCancelContextFunc
    65  //   - ctx nil is panic
    66  //   - ctx not from NewCancelContext or NewCancelContextFunc is panic
    67  //   - thread-safe, idempotent, deferrable
    68  func InvokeCancel(ctx context.Context) {
    69  	invokeCancel(ctx)
    70  }
    71  
    72  // CancelOnError invokes InvokeCancel if errp has an error.
    73  //   - CancelOnError is deferrable and thread-safe.
    74  //   - ctx must have been returned by either NewCancelContext or NewCancelContextFunc.
    75  //   - errp == nil or *errp == nil means no error
    76  //   - ctx nil is panic
    77  //   - ctx not from NewCancelContext or NewCancelContextFunc is panic
    78  //   - thread-safe, idempotent
    79  func CancelOnError(errp *error, ctx context.Context) {
    80  	if errp == nil || *errp == nil {
    81  		return // there was no error
    82  	}
    83  	invokeCancel(ctx)
    84  }
    85  
    86  // invokeCancel is one extra stack frame from invoker
    87  //   - invoked via:
    88  //   - — [parl.InvokeCancel]
    89  //   - — [parl.CancelOnError]
    90  //   - — [parl.OnceWaiter.Cancel]
    91  func invokeCancel(ctx context.Context) {
    92  	if ctx == nil {
    93  		panic(perrors.NewPF("ctx cannot be nil"))
    94  	}
    95  
    96  	// retrieve cancel function from the nearest context.valueCtx
    97  	var cancel context.CancelFunc
    98  	var ok bool
    99  	if cancel, ok = ctx.Value(cancelKey).(context.CancelFunc); !ok {
   100  		panic(perrors.ErrorfPF("%w", ErrNotCancelContext))
   101  	}
   102  
   103  	// invoke the function canceling the context.valueCtx parent that is context.cancelCtx
   104  	//	- and all its child contexts
   105  	cancel()
   106  
   107  	handleContextNotify(ctx)
   108  }