186 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			186 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package lookup
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"sync/atomic"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| type stepFunc func(ctx context.Context, t uint64, hint Epoch) interface{}
 | |
| 
 | |
| // LongEarthLookaheadDelay is the headstart the lookahead gives R before it launches
 | |
| var LongEarthLookaheadDelay = 250 * time.Millisecond
 | |
| 
 | |
| // LongEarthLookbackDelay is the headstart the lookback gives R before it launches
 | |
| var LongEarthLookbackDelay = 250 * time.Millisecond
 | |
| 
 | |
| // LongEarthAlgorithm explores possible lookup paths in parallel, pruning paths as soon
 | |
| // as a more promising lookup path is found. As a result, this lookup algorithm is an order
 | |
| // of magnitude faster than the FluzCapacitor algorithm, but at the expense of more exploratory reads.
 | |
| // This algorithm works as follows. On each step, the next epoch is immediately looked up (R)
 | |
| // and given a head start, while two parallel "steps" are launched a short time after:
 | |
| // look ahead (A) is the path the algorithm would take if the R lookup returns a value, whereas
 | |
| // look back (B) is the path the algorithm would take if the R lookup failed.
 | |
| // as soon as R is actually finished, the A or B paths are pruned depending on the value of R.
 | |
| // if A returns earlier than R, then R and B read operations can be safely canceled, saving time.
 | |
| // The maximum number of active read operations is calculated as 2^(timeout/headstart).
 | |
| // If headstart is infinite, this algorithm behaves as FluzCapacitor.
 | |
| // timeout is the maximum execution time of the passed `read` function.
 | |
| // the two head starts can be configured by changing LongEarthLookaheadDelay or LongEarthLookbackDelay
 | |
| func LongEarthAlgorithm(ctx context.Context, now uint64, hint Epoch, read ReadFunc) (interface{}, error) {
 | |
| 	if hint == NoClue {
 | |
| 		hint = worstHint
 | |
| 	}
 | |
| 
 | |
| 	var stepCounter int32 // for debugging, stepCounter allows to give an ID to each step instance
 | |
| 
 | |
| 	errc := make(chan struct{}) // errc will help as an error shortcut signal
 | |
| 	var gerr error              // in case of error, this variable will be set
 | |
| 
 | |
| 	var step stepFunc // For efficiency, the algorithm step is defined as a closure
 | |
| 	step = func(ctxS context.Context, t uint64, last Epoch) interface{} {
 | |
| 		stepID := atomic.AddInt32(&stepCounter, 1) // give an ID to this call instance
 | |
| 		trace(stepID, "init: t=%d, last=%s", t, last.String())
 | |
| 		var valueA, valueB, valueR interface{}
 | |
| 
 | |
| 		// initialize the three read contexts
 | |
| 		ctxR, cancelR := context.WithCancel(ctxS) // will handle the current read operation
 | |
| 		ctxA, cancelA := context.WithCancel(ctxS) // will handle the lookahead path
 | |
| 		ctxB, cancelB := context.WithCancel(ctxS) // will handle the lookback path
 | |
| 
 | |
| 		epoch := GetNextEpoch(last, t) // calculate the epoch to look up in this step instance
 | |
| 
 | |
| 		// define the lookAhead function, which will follow the path as if R was successful
 | |
| 		lookAhead := func() {
 | |
| 			valueA = step(ctxA, t, epoch) // launch the next step, recursively.
 | |
| 			if valueA != nil {            // if this path is successful, we don't need R or B.
 | |
| 				cancelB()
 | |
| 				cancelR()
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// define the lookBack function, which will follow the path as if R was unsuccessful
 | |
| 		lookBack := func() {
 | |
| 			if epoch.Base() == last.Base() {
 | |
| 				return
 | |
| 			}
 | |
| 			base := epoch.Base()
 | |
| 			if base == 0 {
 | |
| 				return
 | |
| 			}
 | |
| 			valueB = step(ctxB, base-1, last)
 | |
| 		}
 | |
| 
 | |
| 		go func() { //goroutine to read the current epoch (R)
 | |
| 			defer cancelR()
 | |
| 			var err error
 | |
| 			valueR, err = read(ctxR, epoch, now) // read this epoch
 | |
| 			if valueR == nil {                   // if unsuccessful, cancel lookahead, otherwise cancel lookback.
 | |
| 				cancelA()
 | |
| 			} else {
 | |
| 				cancelB()
 | |
| 			}
 | |
| 			if err != nil && err != context.Canceled {
 | |
| 				gerr = err
 | |
| 				close(errc)
 | |
| 			}
 | |
| 		}()
 | |
| 
 | |
| 		go func() { // goroutine to give a headstart to R and then launch lookahead.
 | |
| 			defer cancelA()
 | |
| 
 | |
| 			// if we are at the lowest level or the epoch to look up equals the last one,
 | |
| 			// then we cannot lookahead (can't go lower or repeat the same lookup, this would
 | |
| 			// cause an infinite loop)
 | |
| 			if epoch.Level == LowestLevel || epoch.Equals(last) {
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			// give a head start to R, or launch immediately if R finishes early enough
 | |
| 			select {
 | |
| 			case <-TimeAfter(LongEarthLookaheadDelay):
 | |
| 				lookAhead()
 | |
| 			case <-ctxR.Done():
 | |
| 				if valueR != nil {
 | |
| 					lookAhead() // only look ahead if R was successful
 | |
| 				}
 | |
| 			case <-ctxA.Done():
 | |
| 			}
 | |
| 		}()
 | |
| 
 | |
| 		go func() { // goroutine to give a headstart to R and then launch lookback.
 | |
| 			defer cancelB()
 | |
| 
 | |
| 			// give a head start to R, or launch immediately if R finishes early enough
 | |
| 			select {
 | |
| 			case <-TimeAfter(LongEarthLookbackDelay):
 | |
| 				lookBack()
 | |
| 			case <-ctxR.Done():
 | |
| 				if valueR == nil {
 | |
| 					lookBack() // only look back in case R failed
 | |
| 				}
 | |
| 			case <-ctxB.Done():
 | |
| 			}
 | |
| 		}()
 | |
| 
 | |
| 		<-ctxA.Done()
 | |
| 		if valueA != nil {
 | |
| 			trace(stepID, "Returning valueA=%v", valueA)
 | |
| 			return valueA
 | |
| 		}
 | |
| 
 | |
| 		<-ctxR.Done()
 | |
| 		if valueR != nil {
 | |
| 			trace(stepID, "Returning valueR=%v", valueR)
 | |
| 			return valueR
 | |
| 		}
 | |
| 		<-ctxB.Done()
 | |
| 		trace(stepID, "Returning valueB=%v", valueB)
 | |
| 		return valueB
 | |
| 	}
 | |
| 
 | |
| 	var value interface{}
 | |
| 	stepCtx, cancel := context.WithCancel(ctx)
 | |
| 
 | |
| 	go func() { // launch the root step in its own goroutine to allow cancellation
 | |
| 		defer cancel()
 | |
| 		value = step(stepCtx, now, hint)
 | |
| 	}()
 | |
| 
 | |
| 	// wait for the algorithm to finish, but shortcut in case
 | |
| 	// of errors
 | |
| 	select {
 | |
| 	case <-stepCtx.Done():
 | |
| 	case <-errc:
 | |
| 		cancel()
 | |
| 		return nil, gerr
 | |
| 	}
 | |
| 
 | |
| 	if ctx.Err() != nil {
 | |
| 		return nil, ctx.Err()
 | |
| 	}
 | |
| 
 | |
| 	if value != nil || hint == worstHint {
 | |
| 		return value, nil
 | |
| 	}
 | |
| 
 | |
| 	// at this point the algorithm did not return a value,
 | |
| 	// so we challenge the hint given.
 | |
| 	value, err := read(ctx, hint, now)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if value != nil {
 | |
| 		return value, nil // hint is valid, return it.
 | |
| 	}
 | |
| 
 | |
| 	// hint is invalid. Invoke the algorithm
 | |
| 	// without hint.
 | |
| 	now = hint.Base()
 | |
| 	if hint.Level == HighestLevel {
 | |
| 		now--
 | |
| 	}
 | |
| 
 | |
| 	return LongEarthAlgorithm(ctx, now, NoClue, read)
 | |
| }
 |