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 })