github.com/hasnat/dolt/go@v0.0.0-20210628190320-9eb5d843fbb7/libraries/doltcore/remotestorage/hedge.go (about)

     1  // Copyright 2020 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package remotestorage
    16  
    17  import (
    18  	"context"
    19  	"sync"
    20  	"time"
    21  
    22  	"golang.org/x/sync/semaphore"
    23  
    24  	"github.com/HdrHistogram/hdrhistogram-go"
    25  )
    26  
    27  // Work is a description of work that can be hedged. The supplied Work function
    28  // should expect to potentially be called multiple times concurrently, and it
    29  // should respect |ctx| cancellation. |Size| will be passed to the |Strategy|
    30  // as a parameter to compute the potential hedge retry timeout for this Work.
    31  type Work struct {
    32  	// Work is the function that will be called by |Hedger.Do|. It will be
    33  	// called at least once, and possibly multiple times depending on how
    34  	// long it takes and the |Hedger|'s |Strategy|.
    35  	Work func(ctx context.Context, n int) (interface{}, error)
    36  
    37  	// Size is an integer representation of the size of the work.
    38  	// Potentially used by |Strategy|, not used by |Hedger|.
    39  	Size int
    40  }
    41  
    42  // Hedger can |Do| |Work|, potentially invoking |Work| more than once
    43  // concurrently if it is taking longer than |Strategy| estimated it would.
    44  type Hedger struct {
    45  	sema  *semaphore.Weighted
    46  	strat Strategy
    47  }
    48  
    49  // NewHedger returns a new Hedger. |maxOutstanding| is the most hedged requests
    50  // that can be outstanding. If a request would be hedged, but there are already
    51  // maxOutstanding hedged requests, nothing happens instead.
    52  func NewHedger(maxOutstanding int64, strat Strategy) *Hedger {
    53  	return &Hedger{
    54  		semaphore.NewWeighted(maxOutstanding),
    55  		strat,
    56  	}
    57  }
    58  
    59  // Stategy provides a way estimate the hedge timeout for |Work| given to a
    60  // |Hedger|.
    61  type Strategy interface {
    62  	// Duration returns the expected |time.Duration| of a piece of Work
    63  	// with |Size| |sz|.
    64  	Duration(sz int) time.Duration
    65  	// Observe is called by |Hedger| when work is completed. |sz| is the
    66  	// |Size| of the work. |n| is the nth hedge which completed first, with
    67  	// 1 being the unhedged request. |d| is the duration the |Work|
    68  	// function took for the request that completed. |err| is any |error|
    69  	// returned from |Work|.
    70  	Observe(sz, n int, d time.Duration, err error)
    71  }
    72  
    73  // NewPercentileStrategy returns an initialized |PercentileStrategy| |Hedger|.
    74  func NewPercentileStrategy(low, high time.Duration, perc float64) *PercentileStrategy {
    75  	lowi := int64(low / time.Millisecond)
    76  	highi := int64(high / time.Millisecond)
    77  	return &PercentileStrategy{
    78  		perc,
    79  		hdrhistogram.New(lowi, highi, 3),
    80  		new(sync.Mutex),
    81  	}
    82  }
    83  
    84  // PercentileStrategy is a hedge timeout streategy which puts all |Observe|
    85  // durations into a histogram and returns the current value of the provided
    86  // |Percentile| in that histogram for the estimated |Duration|. |Size| is
    87  // ignored.
    88  type PercentileStrategy struct {
    89  	Percentile float64
    90  	histogram  *hdrhistogram.Histogram
    91  	mu         *sync.Mutex
    92  }
    93  
    94  // Duration implements |Strategy|.
    95  func (ps *PercentileStrategy) Duration(sz int) time.Duration {
    96  	ps.mu.Lock()
    97  	defer ps.mu.Unlock()
    98  	return time.Duration(ps.histogram.ValueAtQuantile(ps.Percentile)) * time.Millisecond
    99  }
   100  
   101  // Observe implements |Strategy|.
   102  func (ps *PercentileStrategy) Observe(sz, n int, d time.Duration, err error) {
   103  	if err == nil {
   104  		ps.mu.Lock()
   105  		defer ps.mu.Unlock()
   106  		ps.histogram.RecordValue(int64(d / time.Millisecond))
   107  	}
   108  }
   109  
   110  // MinStrategy is a hedge timeout strategy that optionally delegates to
   111  // |delegate| and replaces the estimated timeout with |min| if it would be less
   112  // than |min|. If |delegate| is |nil|, it is treated as if it always returned
   113  // 0.
   114  func NewMinStrategy(min time.Duration, delegate Strategy) *MinStrategy {
   115  	return &MinStrategy{
   116  		min,
   117  		delegate,
   118  	}
   119  }
   120  
   121  // MinStrategy optionally delegates to another |Strategy| and clamps its
   122  // |Duration| results to a minimum of |Min|.
   123  type MinStrategy struct {
   124  	// Min is the minimum |time.Duration| that |Duration| should return.
   125  	Min        time.Duration
   126  	underlying Strategy
   127  }
   128  
   129  // Duration implements |Strategy|.
   130  func (ms *MinStrategy) Duration(sz int) time.Duration {
   131  	if ms.underlying == nil {
   132  		return ms.Min
   133  	}
   134  	u := ms.underlying.Duration(sz)
   135  	if u < ms.Min {
   136  		return ms.Min
   137  	}
   138  	return u
   139  }
   140  
   141  // Observe implements |Strategy|.
   142  func (ms *MinStrategy) Observe(sz, n int, d time.Duration, err error) {
   143  	if ms.underlying != nil {
   144  		ms.underlying.Observe(sz, n, d, err)
   145  	}
   146  }
   147  
   148  var MaxHedgesPerRequest = 1
   149  
   150  // Do runs |w| to completion, potentially spawning concurrent hedge runs of it.
   151  // Returns the results from the first invocation that completes, and cancels
   152  // the contexts of all invocations.
   153  func (h *Hedger) Do(ctx context.Context, w Work) (interface{}, error) {
   154  	var cancels []func()
   155  	type res struct {
   156  		v interface{}
   157  		e error
   158  		n int
   159  		d time.Duration
   160  	}
   161  	ch := make(chan res)
   162  	try := func() {
   163  		n := len(cancels) + 1
   164  		finalize := func() {}
   165  		if n-1 > MaxHedgesPerRequest {
   166  			return
   167  		}
   168  		if n > 1 {
   169  			if !h.sema.TryAcquire(1) {
   170  				// Too many outstanding hedges. Do nothing.
   171  				return
   172  			}
   173  			finalize = func() {
   174  				h.sema.Release(1)
   175  			}
   176  		}
   177  		ctx, cancel := context.WithCancel(ctx)
   178  		cancels = append(cancels, cancel)
   179  		start := time.Now()
   180  		go func() {
   181  			defer finalize()
   182  			v, e := w.Work(ctx, n)
   183  			select {
   184  			case ch <- res{v, e, n, time.Since(start)}:
   185  			case <-ctx.Done():
   186  			}
   187  		}()
   188  	}
   189  	try()
   190  	for {
   191  		nextTry := h.strat.Duration(w.Size) * (1 << len(cancels))
   192  		select {
   193  		case r := <-ch:
   194  			for _, c := range cancels {
   195  				c()
   196  			}
   197  			h.strat.Observe(w.Size, r.n, r.d, r.e)
   198  			return r.v, r.e
   199  		case <-time.After(nextTry):
   200  			try()
   201  		case <-ctx.Done():
   202  			return nil, ctx.Err()
   203  		}
   204  	}
   205  }