github.com/haraldrudell/parl@v0.4.176/win-or-waiter-core.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  	"time"
    12  
    13  	"github.com/haraldrudell/parl/perrors"
    14  	"github.com/haraldrudell/parl/sets"
    15  )
    16  
    17  const (
    18  	// WinOrWaiterAnyValue allows a thread to accept any calculated value
    19  	WinOrWaiterAnyValue WinOrWaiterStrategy = iota
    20  	// WinOrWaiterMustBeLater forces a calculation commencing after a thread arrives
    21  	//	- WinOrWaiter caclulations are serialized, ie. a new calculation does not start prior to
    22  	//		the conclusion of the previous calulation
    23  	//	- thread arrival time is prior to acquiring the lock
    24  	WinOrWaiterMustBeLater
    25  )
    26  
    27  // WinOrWaiter picks a winner thread to carry out some task used by many threads.
    28  //   - threads in WinOrWait for an idle WinorWaiter may become winners completing the task
    29  //   - threads in WinOrWait while a calculation is in progress are held waiting using
    30  //     RWLock and atomics until the calculation completes
    31  //   - the calculation is completed on demand, but only by the first requesting thread
    32  //   - —
    33  //   - mechanic is closing channel inside Awaitable inside Future
    34  //   - threads await a renewable Future until a new-enough result occurs
    35  //   - of the threads requring a newer calculation, one is selected to perform the calculation
    36  type WinOrWaiterCore struct {
    37  	// calculator if the function making the calculation
    38  	calculator func() (err error)
    39  	// calculation strategy for this WinOrWaiter
    40  	//	- WinOrWaiterAnyValue WinOrWaiterMustBeLater
    41  	strategy WinOrWaiterStrategy
    42  	// context used for cancellation, may be nil
    43  	ctx context.Context
    44  
    45  	// isCalculationPut indicates that calculation field has value. atomic access
    46  	isCalculationPut atomic.Bool
    47  	// calculationPut makes threads wait until calculation has value
    48  	calculationPut Once
    49  	// calculation allow to wait for the result of a winner calculation
    50  	//	- winner holds lock.Lock until the calculation is complete
    51  	//	- loser threads wait for lock.RLock to check the result
    52  	calculation atomic.Pointer[Future[time.Time]]
    53  
    54  	// winnerPicker picks winner thread using atomic access
    55  	//	- winner is the thread that on Set gets wasNotSet true
    56  	//	- true while a winner calculates next data value
    57  	//	- set to zero when winnerFunc returns
    58  	winnerPicker atomic.Bool
    59  }
    60  
    61  // WinOrWaiter returns a semaphore used for completing an on-demand task by
    62  // the first thread requesting it, and that result shared by subsequent threads held
    63  // waiting for the result.
    64  //   - strategy: WinOrWaiterAnyValue WinOrWaiterMustBeLater
    65  //   - ctx allows foir cancelation of the WinOrWaiter
    66  func NewWinOrWaiterCore(strategy WinOrWaiterStrategy, calculator func() (err error), ctx ...context.Context) (winOrWaiter *WinOrWaiterCore) {
    67  	if !strategy.IsValid() {
    68  		panic(perrors.ErrorfPF("Bad WinOrWaiter strategy: %s", strategy))
    69  	}
    70  	if calculator == nil {
    71  		panic(perrors.ErrorfPF("calculator function cannot be nil"))
    72  	}
    73  	var ctx0 context.Context
    74  	if len(ctx) > 0 {
    75  		ctx0 = ctx[0]
    76  	}
    77  	return &WinOrWaiterCore{
    78  		strategy:   strategy,
    79  		calculator: calculator,
    80  		ctx:        ctx0,
    81  	}
    82  }
    83  
    84  // WinOrWaiter picks a winner thread to carry out some task used by many threads.
    85  //   - threads arriving to an idle WinorWaiter are winners that complete the task
    86  //   - threads arriving to a WinOrWait in progress are held waiting at RWMutex
    87  //   - the task is completed on demand, but only by the first thread requesting it
    88  func (ww *WinOrWaiterCore) WinOrWait() (err error) {
    89  
    90  	// arrivalTime is the time this thread arrived
    91  	var arrivalTime = time.Now()
    92  
    93  	// ensure WinOrWaiter has calculator function
    94  	if ww == nil || ww.calculator == nil {
    95  		err = perrors.NewPF("WinOrWait for nil or uninitialized WinOrWaiter")
    96  		return
    97  	}
    98  	// seenCalculation is the calculation present when this thread arrived.
    99  	// seenCalculation may be nil
   100  	var seenCalculation = ww.calculation.Load()
   101  
   102  	// ensure that ww.calculation holds a calculation
   103  	if !ww.isCalculationPut.Load() {
   104  
   105  		// invocation prior to first calculation started
   106  		// start the first calculation, or wait for it to be started if another thread already started it
   107  		var didOnce bool
   108  		if didOnce, _, err = ww.calculationPut.DoErr(ww.winnerFunc); didOnce {
   109  			return // thread did initial winner calculation return
   110  		}
   111  		err = nil // subsequent threads do not report possible error
   112  	}
   113  
   114  	// wait for late-enough data
   115  	var calculation *Future[time.Time]
   116  	for {
   117  
   118  		// check for valid calculation result
   119  		calculation = ww.calculation.Load()
   120  
   121  		// calculation.Result: block here
   122  		if result, isValid := calculation.Result(); isValid {
   123  			switch ww.strategy {
   124  			case WinOrWaiterAnyValue:
   125  				if calculation != seenCalculation {
   126  					return // any new valid value accepted return
   127  				}
   128  			case WinOrWaiterMustBeLater:
   129  				if !result.Before(arrivalTime) {
   130  					// arrival time the same or after dataVersionNow
   131  					return // must be later and the data version is of a later time than when this thread arrived return
   132  				}
   133  			}
   134  		}
   135  
   136  		// ensure data processing is in progress
   137  		if isWinner := ww.winnerPicker.CompareAndSwap(false, true); isWinner {
   138  			return ww.winnerFunc() // this thread completed the task return
   139  		}
   140  
   141  		// check context cancelation
   142  		if ww.IsCancel() {
   143  			return // context canceled return
   144  		}
   145  	}
   146  }
   147  
   148  func (ww *WinOrWaiterCore) IsCancel() (isCancel bool) {
   149  	return ww.ctx != nil && ww.ctx.Err() != nil
   150  }
   151  
   152  func (ww *WinOrWaiterCore) winnerFunc() (err error) {
   153  	ww.winnerPicker.Store(true)
   154  	defer ww.winnerPicker.Store(false)
   155  
   156  	// get calculation
   157  	var calculation = NewFuture[time.Time]()
   158  	ww.calculation.Store(calculation)
   159  	ww.isCalculationPut.Store(true)
   160  
   161  	// calculate
   162  	result := time.Now()
   163  	defer calculation.End(&result, nil, &err)
   164  	defer RecoverErr(func() DA { return A() }, &err)
   165  
   166  	err = ww.calculator()
   167  
   168  	return
   169  }
   170  
   171  type WinOrWaiterStrategy uint8
   172  
   173  func (ws WinOrWaiterStrategy) String() (s string) {
   174  	return winOrWaiterSet.StringT(ws)
   175  }
   176  
   177  func (ws WinOrWaiterStrategy) IsValid() (isValid bool) {
   178  	return winOrWaiterSet.IsValid(ws)
   179  }
   180  
   181  var winOrWaiterSet = sets.NewSet[WinOrWaiterStrategy]([]sets.SetElement[WinOrWaiterStrategy]{
   182  	{ValueV: WinOrWaiterAnyValue, Name: "anyValue"},
   183  	{ValueV: WinOrWaiterMustBeLater, Name: "mustBeLater"},
   184  })