harbor/src/lib/retry/retry.go

167 lines
3.6 KiB
Go

// Copyright Project Harbor Authors
//
// Licensed 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 retry
import (
"fmt"
"math/rand"
"time"
"github.com/jpillora/backoff"
"github.com/goharbor/harbor/src/lib/errors"
)
func init() {
rand.NewSource(time.Now().UnixNano())
}
var (
// ErrRetryTimeout timeout error for retrying
ErrRetryTimeout = errors.New("retry timeout")
)
type abort struct {
cause error
}
func (a *abort) Error() string {
if a.cause != nil {
return fmt.Sprintf("retry abort, error: %v", a.cause)
}
return "retry abort"
}
// Abort wrap err to stop the Retry function
func Abort(err error) error {
return &abort{cause: err}
}
// Options options for the retry functions
type Options struct {
InitialInterval time.Duration // the initial interval for retring after failure, default 100 milliseconds
MaxInterval time.Duration // the max interval for retring after failure, default 1 second
Timeout time.Duration // the total time before returning if something is wrong, default 1 minute
Callback func(err error, sleep time.Duration) // the callback function for Retry when the f called failed
Backoff bool
}
// Option ...
type Option func(*Options)
// InitialInterval set initial interval
func InitialInterval(initial time.Duration) Option {
return func(opts *Options) {
opts.InitialInterval = initial
}
}
// MaxInterval set max interval
func MaxInterval(max time.Duration) Option {
return func(opts *Options) {
opts.MaxInterval = max
}
}
// Timeout set timeout interval
func Timeout(timeout time.Duration) Option {
return func(opts *Options) {
opts.Timeout = timeout
}
}
// Callback set callback
func Callback(callback func(err error, sleep time.Duration)) Option {
return func(opts *Options) {
opts.Callback = callback
}
}
// Backoff set backoff
func Backoff(backoff bool) Option {
return func(opts *Options) {
opts.Backoff = backoff
}
}
// Retry retry until f run successfully or timeout
//
// NOTE: This function will use exponential backoff and jitter for retrying, see
// https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ for more information
func Retry(f func() error, options ...Option) error {
opts := &Options{
Backoff: true,
}
for _, o := range options {
o(opts)
}
if opts.InitialInterval <= 0 {
opts.InitialInterval = time.Millisecond * 100
}
if opts.MaxInterval <= 0 {
opts.MaxInterval = time.Second
}
if opts.Timeout <= 0 {
opts.Timeout = time.Minute
}
var b *backoff.Backoff
if opts.Backoff {
b = &backoff.Backoff{
Min: opts.InitialInterval,
Max: opts.MaxInterval,
Factor: 2,
Jitter: true,
}
}
var err error
timeout := time.After(opts.Timeout)
for {
select {
case <-timeout:
return errors.New(ErrRetryTimeout).WithCause(err)
default:
err = f()
if err == nil {
return nil
}
var ab *abort
if errors.As(err, &ab) {
return ab.cause
}
var sleep time.Duration
if opts.Backoff {
sleep = b.Duration()
}
if opts.Callback != nil {
opts.Callback(err, sleep)
}
time.Sleep(sleep)
}
}
}