github.com/haraldrudell/parl@v0.4.176/ptime/invoke-on-timer.go (about)

     1  /*
     2  © 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package ptime
     7  
     8  import (
     9  	"time"
    10  
    11  	"github.com/haraldrudell/parl/internal/cyclebreaker"
    12  	"github.com/haraldrudell/parl/perrors"
    13  )
    14  
    15  const (
    16  	defaultInvokeOnTimerDuration = 100 * time.Millisecond
    17  )
    18  
    19  // InvokeOnTimer implements a timeout action
    20  type InvokeOnTimer struct {
    21  	awaitable *cyclebreaker.Awaitable
    22  }
    23  
    24  // NewInvokeOnTimer panics or invokes elapsedFunc if not reset within d
    25  //   - default d 100 ms
    26  //   - on timer expiry:
    27  //   - — if errPanic is non-nil, process-terminating panic
    28  //   - — if elapsedFunc is non-nil, it is invoked.
    29  //     Panics in elapsedFunc are recovered
    30  //   - errPanic and elapsedFunc cannot both be nil
    31  //   - each NewInvokeOnTimer launches a separate, blocked thread
    32  //   - if errCh is present it should be a buffered channel receving
    33  //     a value on thread-exit that may be a panic from the thread.
    34  //     the thread should not have any panics but a panic may occur in
    35  //     elapsedFunc.
    36  //   - if errCh is missing or nil, panic is echoed to stderr
    37  //   - resources are released on:
    38  //   - — Stop invocation
    39  //   - — timer firing
    40  func NewInvokeOnTimer(d time.Duration, errPanic error, elapsedFunc func(), errCh ...chan<- error) (timer *InvokeOnTimer) {
    41  	if errPanic == nil && elapsedFunc == nil {
    42  		cyclebreaker.NilError("errPanic and elapsedFunc")
    43  	}
    44  	if d <= 0 {
    45  		d = defaultInvokeOnTimerDuration
    46  	}
    47  	var errCh0 chan<- error
    48  	if len(errCh) > 0 {
    49  		errCh0 = errCh[0]
    50  	}
    51  
    52  	var t = InvokeOnTimer{awaitable: cyclebreaker.NewAwaitable()}
    53  	go invokeOnTimerThread(
    54  		time.NewTimer(d), t.awaitable.Ch(),
    55  		errPanic, elapsedFunc,
    56  		errCh0,
    57  	)
    58  	return &t
    59  }
    60  
    61  // Stop cancels the timer and releases resources
    62  func (t *InvokeOnTimer) Stop() { t.awaitable.Close() }
    63  
    64  // the thread handling timer expiry
    65  func invokeOnTimerThread(
    66  	timer *time.Timer, c cyclebreaker.AwaitableCh,
    67  	errPanic error, elapsedFunc func(),
    68  	errCh chan<- error,
    69  ) {
    70  	var err error
    71  	if errCh != nil {
    72  		defer cyclebreaker.SendErr(errCh, &err)
    73  	} else {
    74  		defer infallible(&err)
    75  	}
    76  	// errPanic non-nil causes process termination by panic outside of RecoverErr
    77  	if errPanic != nil {
    78  		defer processTermination(&errPanic)
    79  	}
    80  	defer cyclebreaker.RecoverErr(func() cyclebreaker.DA { return cyclebreaker.A() }, &err)
    81  	defer timer.Stop()
    82  
    83  	select {
    84  	case <-timer.C:
    85  		if errPanic != nil {
    86  			return // terminate the process
    87  		}
    88  		elapsedFunc() // panic will be recovered
    89  	case <-c:
    90  		errPanic = nil
    91  	}
    92  }
    93  
    94  func infallible(errp *error) {
    95  	var err = *errp
    96  	if err == nil {
    97  		return
    98  	}
    99  	cyclebreaker.Infallible(err)
   100  }
   101  
   102  func processTermination(errp *error) {
   103  	var err = *errp
   104  	if err == nil {
   105  		return
   106  	}
   107  	// if the err has stack, ensure it is printed
   108  	if perrors.HasStack(err) {
   109  		cyclebreaker.Log(perrors.Long(err))
   110  	}
   111  	panic(err)
   112  }