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 }