...

Source file src/cloud.google.com/go/cloudsqlconn/internal/cloudsql/retry.go

Documentation: cloud.google.com/go/cloudsqlconn/internal/cloudsql

     1  // Copyright 2024 Google LLC
     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  //     https://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 cloudsql
    16  
    17  import (
    18  	"context"
    19  	"math"
    20  	"math/rand"
    21  	"time"
    22  
    23  	"google.golang.org/api/googleapi"
    24  )
    25  
    26  // exponentialBackoff calculates a duration based on the attempt i.
    27  //
    28  // The formula is:
    29  //
    30  //	base * multi^(attempt + 1 + random)
    31  //
    32  // With base = 200ms and multi = 1.1618, and random = [0.0, 1.0),
    33  // the backoff values would fall between the following low and high ends:
    34  //
    35  // Attempt  Low (ms)  High (ms)
    36  //
    37  //	0         324	     524
    38  //	1         524	     847
    39  //	2         847	    1371
    40  //	3        1371	    2218
    41  //	4        2218	    3588
    42  //
    43  // The theoretical worst case scenario would have a client wait 8.5s in total
    44  // for an API request to complete (with the first four attempts failing, and
    45  // the fifth succeeding).
    46  //
    47  // This backoff strategy matches the behavior of the Cloud SQL Proxy v1.
    48  func exponentialBackoff(attempt int) time.Duration {
    49  	const (
    50  		base  = float64(200 * time.Millisecond)
    51  		multi = 1.618
    52  	)
    53  	exp := float64(attempt+1) + rand.Float64()
    54  	return time.Duration(base * math.Pow(multi, exp))
    55  }
    56  
    57  // retry50x will retry any 50x HTTP response up to maxRetries times. The
    58  // backoffFunc determines the duration to wait between attempts.
    59  func retry50x[T any](
    60  	ctx context.Context,
    61  	f func(context.Context) (*T, error),
    62  	waitDuration func(int) time.Duration,
    63  ) (*T, error) {
    64  	const maxRetries = 5
    65  	var (
    66  		resp *T
    67  		err  error
    68  	)
    69  	for i := 0; i < maxRetries; i++ {
    70  		resp, err = f(ctx)
    71  		// If err is nil, break and return the response.
    72  		if err == nil {
    73  			break
    74  		}
    75  
    76  		gErr, ok := err.(*googleapi.Error)
    77  		// If err is not a googleapi.Error, don't retry.
    78  		if !ok {
    79  			return nil, err
    80  		}
    81  		// If the error code is not a 50x error, don't retry.
    82  		if gErr.Code < 500 {
    83  			return nil, err
    84  		}
    85  
    86  		if wErr := wait(ctx, waitDuration(i)); wErr != nil {
    87  			err = wErr
    88  			break
    89  		}
    90  
    91  	}
    92  	return resp, err
    93  }
    94  
    95  // wait will block until the provided duration passes or the context is
    96  // canceled, whatever happens first.
    97  func wait(ctx context.Context, d time.Duration) error {
    98  	timer := time.NewTimer(d)
    99  	select {
   100  	case <-ctx.Done():
   101  		if !timer.Stop() {
   102  			<-timer.C
   103  		}
   104  		return ctx.Err()
   105  	case <-timer.C:
   106  		return nil
   107  	}
   108  }
   109  

View as plain text