retryutil/retry.go (90 lines of code) (raw):

// Copyright 2019 Google Inc. All Rights Reserved. // // 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 retryutil provides utility functions for retrying. package retryutil import ( "context" "errors" "fmt" "math" "math/rand" "time" "cloud.google.com/go/compute/metadata" "github.com/GoogleCloudPlatform/osconfig/clog" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) var currentSleeper sleeper = defaultSleeper{} // RetrySleep returns a pseudo-random sleep duration. func RetrySleep(base int, extra int) time.Duration { // base=1 and extra=0 => 1*1+[0,1] => 1-2s // base=2 and extra=0 => 2*2+[0,2] => 4-6s // base=3 and extra=0 => 3*3+[0,3] => 9-12s // base=1 and extra=5 => 6*1+[0,6] => 6-12s // base=2 and extra=5 => 7*2+[0,7] => 14-21s // base=3 and extra=5 => 8*3+[0,8] => 24-32s // base=1 and extra=10 => 11*1+[0,11] => 11-22s // base=2 and extra=10 => 12*2+[0,12] => 24-36s // base=3 and extra=10 => 13*3+[0,13] => 39-52s rnd := rand.New(rand.NewSource(time.Now().UnixNano())) nf := math.Min(float64((base+extra)*base+rnd.Intn(base+extra)), 300) return time.Duration(int(nf)) * time.Second } // RetryFunc retries a function provided as a parameter for maxRetryTime. func RetryFunc(ctx context.Context, maxRetryTime time.Duration, desc string, f func() error) error { var tot time.Duration for i := 1; ; i++ { err := f() if err == nil { return nil } ns := RetrySleep(i, 0) tot += ns if tot > maxRetryTime { return err } clog.Errorf(ctx, "Error %s, attempt %d, retrying in %s: %v", desc, i, ns, err) currentSleeper.Sleep(ns) } } // RetryAPICall retries an API call for maxRetryTime. func RetryAPICall(ctx context.Context, maxRetryTime time.Duration, name string, f func() error) error { var tot time.Duration for i := 1; ; i++ { extra := 1 err := f() if err == nil { return nil } s, ok := status.FromError(err) if !ok { // Non API errors are not retried return err } if !isRetriable(s) { return humanReadableError(err, s) } if isResourceExhausted(s) { extra = 10 } ns := RetrySleep(i, extra) tot += ns if tot > maxRetryTime { // Return human readable error return errorFromStatus(s) } clog.Warningf(ctx, "Error calling %s, attempt %d, retrying in %s: %v", name, i, ns, err) currentSleeper.Sleep(ns) } } func humanReadableError(err error, s *status.Status) error { var ndr *metadata.NotDefinedError if errors.As(err, &ndr) { return fmt.Errorf("no service account set for instance") } return errorFromStatus(s) } func errorFromStatus(s *status.Status) error { return fmt.Errorf("code: %q, message: %q, details: %q", s.Code(), s.Message(), s.Details()) } func isRetriable(s *status.Status) bool { switch s.Code() { case codes.Aborted, codes.DeadlineExceeded, codes.Internal, codes.ResourceExhausted, codes.Unavailable: return true default: return false } } func isResourceExhausted(s *status.Status) bool { return s.Code() == codes.ResourceExhausted } type sleeper interface { Sleep(d time.Duration) } type defaultSleeper struct{} func (ds defaultSleeper) Sleep(d time.Duration) { time.Sleep(d) }