-
Notifications
You must be signed in to change notification settings - Fork 286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: optimize retry #284
feat: optimize retry #284
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
/* | ||
* Licensed to the Apache Software Foundation (ASF) under one or more | ||
* contributor license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright ownership. | ||
* The ASF licenses this file to You under the Apache License, Version 2.0 | ||
* (the "License"); you may not use this file except in compliance with | ||
* the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package backoff | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"fmt" | ||
"math/rand" | ||
"time" | ||
) | ||
|
||
// Config configures a Backoff | ||
type Config struct { | ||
MinBackoff time.Duration `yaml:"min_period"` // start backoff at this level | ||
MaxBackoff time.Duration `yaml:"max_period"` // increase exponentially to this level | ||
MaxRetries int `yaml:"max_retries"` // give up after this many; zero means infinite retries | ||
} | ||
|
||
// RegisterFlagsWithPrefix for Config. | ||
func (cfg *Config) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) { | ||
f.DurationVar(&cfg.MinBackoff, prefix+".backoff-min-period", 100*time.Millisecond, "Minimum delay when backing off.") | ||
f.DurationVar(&cfg.MaxBackoff, prefix+".backoff-max-period", 10*time.Second, "Maximum delay when backing off.") | ||
f.IntVar(&cfg.MaxRetries, prefix+".backoff-retries", 10, "Number of times to backoff and retry before failing.") | ||
} | ||
|
||
// Backoff implements exponential backoff with randomized wait times | ||
type Backoff struct { | ||
cfg Config | ||
ctx context.Context | ||
numRetries int | ||
nextDelayMin time.Duration | ||
nextDelayMax time.Duration | ||
} | ||
|
||
// New creates a Backoff object. Pass a Context that can also terminate the operation. | ||
func New(ctx context.Context, cfg Config) *Backoff { | ||
return &Backoff{ | ||
cfg: cfg, | ||
ctx: ctx, | ||
nextDelayMin: cfg.MinBackoff, | ||
nextDelayMax: doubleDuration(cfg.MinBackoff, cfg.MaxBackoff), | ||
} | ||
} | ||
|
||
// Reset the Backoff back to its initial condition | ||
func (b *Backoff) Reset() { | ||
b.numRetries = 0 | ||
b.nextDelayMin = b.cfg.MinBackoff | ||
b.nextDelayMax = doubleDuration(b.cfg.MinBackoff, b.cfg.MaxBackoff) | ||
} | ||
|
||
// Ongoing returns true if caller should keep going | ||
func (b *Backoff) Ongoing() bool { | ||
// Stop if Context has errored or max retry count is exceeded | ||
return b.ctx.Err() == nil && (b.cfg.MaxRetries == 0 || b.numRetries < b.cfg.MaxRetries) | ||
} | ||
|
||
// Err returns the reason for terminating the backoff, or nil if it didn't terminate | ||
func (b *Backoff) Err() error { | ||
if b.ctx.Err() != nil { | ||
return b.ctx.Err() | ||
} | ||
if b.cfg.MaxRetries != 0 && b.numRetries >= b.cfg.MaxRetries { | ||
return fmt.Errorf("terminated after %d retries", b.numRetries) | ||
} | ||
return nil | ||
} | ||
|
||
// NumRetries returns the number of retries so far | ||
func (b *Backoff) NumRetries() int { | ||
return b.numRetries | ||
} | ||
|
||
// Wait sleeps for the backoff time then increases the retry count and backoff time | ||
// Returns immediately if Context is terminated | ||
func (b *Backoff) Wait() { | ||
// Increase the number of retries and get the next delay | ||
sleepTime := b.NextDelay() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This statement can be put into the if range There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 举个例子 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
感觉是个优化项,不是问题,比如: if b.Ongoing() { There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 你这个不太好。目前这个实现已经很简洁了 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 使用起来也很简单。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 好的, 我A了 |
||
|
||
if b.Ongoing() { | ||
select { | ||
case <-b.ctx.Done(): | ||
case <-time.After(sleepTime): | ||
} | ||
} | ||
} | ||
|
||
func (b *Backoff) NextDelay() time.Duration { | ||
b.numRetries++ | ||
|
||
// Handle the edge case where the min and max have the same value | ||
// (or due to some misconfig max is < min) | ||
if b.nextDelayMin >= b.nextDelayMax { | ||
return b.nextDelayMin | ||
} | ||
|
||
// Add a jitter within the next exponential backoff range | ||
sleepTime := b.nextDelayMin + time.Duration(rand.Int63n(int64(b.nextDelayMax-b.nextDelayMin))) | ||
|
||
// Apply the exponential backoff to calculate the next jitter | ||
// range, unless we've already reached the max | ||
if b.nextDelayMax < b.cfg.MaxBackoff { | ||
b.nextDelayMin = doubleDuration(b.nextDelayMin, b.cfg.MaxBackoff) | ||
b.nextDelayMax = doubleDuration(b.nextDelayMax, b.cfg.MaxBackoff) | ||
} | ||
|
||
return sleepTime | ||
} | ||
|
||
func doubleDuration(value time.Duration, max time.Duration) time.Duration { | ||
value = value * 2 | ||
|
||
if value <= max { | ||
return value | ||
} | ||
|
||
return max | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
/* | ||
* Licensed to the Apache Software Foundation (ASF) under one or more | ||
* contributor license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright ownership. | ||
* The ASF licenses this file to You under the Apache License, Version 2.0 | ||
* (the "License"); you may not use this file except in compliance with | ||
* the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package backoff | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func TestBackoff_NextDelay(t *testing.T) { | ||
t.Parallel() | ||
|
||
tests := map[string]struct { | ||
minBackoff time.Duration | ||
maxBackoff time.Duration | ||
expectedRanges [][]time.Duration | ||
}{ | ||
"exponential backoff with jitter honoring min and max": { | ||
minBackoff: 100 * time.Millisecond, | ||
maxBackoff: 10 * time.Second, | ||
expectedRanges: [][]time.Duration{ | ||
{100 * time.Millisecond, 200 * time.Millisecond}, | ||
{200 * time.Millisecond, 400 * time.Millisecond}, | ||
{400 * time.Millisecond, 800 * time.Millisecond}, | ||
{800 * time.Millisecond, 1600 * time.Millisecond}, | ||
{1600 * time.Millisecond, 3200 * time.Millisecond}, | ||
{3200 * time.Millisecond, 6400 * time.Millisecond}, | ||
{6400 * time.Millisecond, 10000 * time.Millisecond}, | ||
{6400 * time.Millisecond, 10000 * time.Millisecond}, | ||
}, | ||
}, | ||
"exponential backoff with max equal to the end of a range": { | ||
minBackoff: 100 * time.Millisecond, | ||
maxBackoff: 800 * time.Millisecond, | ||
expectedRanges: [][]time.Duration{ | ||
{100 * time.Millisecond, 200 * time.Millisecond}, | ||
{200 * time.Millisecond, 400 * time.Millisecond}, | ||
{400 * time.Millisecond, 800 * time.Millisecond}, | ||
{400 * time.Millisecond, 800 * time.Millisecond}, | ||
}, | ||
}, | ||
"exponential backoff with max equal to the end of a range + 1": { | ||
minBackoff: 100 * time.Millisecond, | ||
maxBackoff: 801 * time.Millisecond, | ||
expectedRanges: [][]time.Duration{ | ||
{100 * time.Millisecond, 200 * time.Millisecond}, | ||
{200 * time.Millisecond, 400 * time.Millisecond}, | ||
{400 * time.Millisecond, 800 * time.Millisecond}, | ||
{800 * time.Millisecond, 801 * time.Millisecond}, | ||
{800 * time.Millisecond, 801 * time.Millisecond}, | ||
}, | ||
}, | ||
"exponential backoff with max equal to the end of a range - 1": { | ||
minBackoff: 100 * time.Millisecond, | ||
maxBackoff: 799 * time.Millisecond, | ||
expectedRanges: [][]time.Duration{ | ||
{100 * time.Millisecond, 200 * time.Millisecond}, | ||
{200 * time.Millisecond, 400 * time.Millisecond}, | ||
{400 * time.Millisecond, 799 * time.Millisecond}, | ||
{400 * time.Millisecond, 799 * time.Millisecond}, | ||
}, | ||
}, | ||
"min backoff is equal to max": { | ||
minBackoff: 100 * time.Millisecond, | ||
maxBackoff: 100 * time.Millisecond, | ||
expectedRanges: [][]time.Duration{ | ||
{100 * time.Millisecond, 100 * time.Millisecond}, | ||
{100 * time.Millisecond, 100 * time.Millisecond}, | ||
{100 * time.Millisecond, 100 * time.Millisecond}, | ||
}, | ||
}, | ||
"min backoff is greater then max": { | ||
minBackoff: 200 * time.Millisecond, | ||
maxBackoff: 100 * time.Millisecond, | ||
expectedRanges: [][]time.Duration{ | ||
{200 * time.Millisecond, 200 * time.Millisecond}, | ||
{200 * time.Millisecond, 200 * time.Millisecond}, | ||
{200 * time.Millisecond, 200 * time.Millisecond}, | ||
}, | ||
}, | ||
} | ||
|
||
for testName, testData := range tests { | ||
testData := testData | ||
|
||
t.Run(testName, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
b := New(context.Background(), Config{ | ||
MinBackoff: testData.minBackoff, | ||
MaxBackoff: testData.maxBackoff, | ||
MaxRetries: len(testData.expectedRanges), | ||
}) | ||
|
||
for _, expectedRange := range testData.expectedRanges { | ||
delay := b.NextDelay() | ||
|
||
if delay < expectedRange[0] || delay > expectedRange[1] { | ||
t.Errorf("%d expected to be within %d and %d", delay, expectedRange[0], expectedRange[1]) | ||
} | ||
} | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个log级别是不是应该提升为 Error 级别。另外这个log和115行的log感觉是重复的,保留一个应该就可以了。