internal/cloudsql/retry.go (57 lines of code) (raw):

// Copyright 2024 Google LLC // // 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 // // https://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 cloudsql import ( "context" "math" "math/rand" "time" "google.golang.org/api/googleapi" ) // exponentialBackoff calculates a duration based on the attempt i. // // The formula is: // // base * multi^(attempt + 1 + random) // // With base = 200ms and multi = 1.618, and random = [0.0, 1.0), // the backoff values would fall between the following low and high ends: // // Attempt Low (ms) High (ms) // // 0 324 524 // 1 524 847 // 2 847 1371 // 3 1371 2218 // 4 2218 3588 // // The theoretical worst case scenario would have a client wait 8.5s in total // for an API request to complete (with the first four attempts failing, and // the fifth succeeding). // // This backoff strategy matches the behavior of the Cloud SQL Proxy v1. func exponentialBackoff(attempt int) time.Duration { const ( base = float64(200 * time.Millisecond) multi = 1.618 ) exp := float64(attempt+1) + rand.Float64() return time.Duration(base * math.Pow(multi, exp)) } // retry50x will retry any 50x HTTP response up to maxRetries times. The // backoffFunc determines the duration to wait between attempts. func retry50x[T any]( ctx context.Context, f func(context.Context) (*T, error), waitDuration func(int) time.Duration, ) (*T, error) { const maxRetries = 5 var ( resp *T err error ) for i := 0; i < maxRetries; i++ { resp, err = f(ctx) // If err is nil, break and return the response. if err == nil { break } gErr, ok := err.(*googleapi.Error) // If err is not a googleapi.Error, don't retry. if !ok { return nil, err } // If the error code is not a 50x error, don't retry. if gErr.Code < 500 { return nil, err } if wErr := wait(ctx, waitDuration(i)); wErr != nil { err = wErr break } } return resp, err } // wait will block until the provided duration passes or the context is // canceled, whatever happens first. func wait(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) select { case <-ctx.Done(): if !timer.Stop() { <-timer.C } return ctx.Err() case <-timer.C: return nil } }