sigs.k8s.io/release-sdk@v0.11.1-0.20240417074027-8061fb5e4952/github/internal/retry.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package internal
    18  
    19  import (
    20  	"crypto/rand"
    21  	"math/big"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/google/go-github/v60/github"
    26  	"github.com/sirupsen/logrus"
    27  )
    28  
    29  const (
    30  	// MaxGithubRetries is the maximum amount of times we flag a GitHub error as
    31  	// retryable before we give up and do not flag the same call as retryable
    32  	// anymore.
    33  	MaxGithubRetries = 3
    34  
    35  	// defaultGithubSleep is the amount of time we wait between two consecutive
    36  	// GitHub calls in case we cannot extract that information from the error
    37  	// itself.
    38  	defaultGithubSleep = time.Minute
    39  )
    40  
    41  // DefaultGithubErrChecker is a GithubErrChecker set up with a default amount
    42  // of retries and the default sleep function.
    43  func DefaultGithubErrChecker() func(error) bool {
    44  	return GithubErrChecker(MaxGithubRetries, time.Sleep)
    45  }
    46  
    47  // GithubErrChecker returns a function that checks errors from GitHub and
    48  // decides if they can / should be retried.
    49  // It needs to be called with `maxTries`, a number of retries a single call
    50  // should be retried at max, and `sleeper`, a function which implements the
    51  // sleeping.
    52  //
    53  // Currently only the `github.RateLimitError` and `github.AbuseRateLimitError`
    54  // return values are supported. If one of those errors occur, then we sleep for
    55  // a default duration or the amount of time the error told us to wait.
    56  //
    57  // Other special errors should be easy to implement too.
    58  //
    59  // It can be used like this:
    60  //
    61  //	for shouldRetry := GithubErrChecker(10, time.Sleep); ; {
    62  //	  commit, res, err := github_client.GetCommit(...)
    63  //	  if !shouldRetry(err) {
    64  //	    return commit, res, err
    65  //	  }
    66  //	}
    67  func GithubErrChecker(maxTries int, sleeper func(time.Duration)) func(error) bool {
    68  	try := 0
    69  
    70  	return func(err error) bool {
    71  		if err == nil {
    72  			return false
    73  		}
    74  		if try >= maxTries {
    75  			logrus.Errorf("Max retries (%d) reached, not retrying anymore: %v", maxTries, err)
    76  			return false
    77  		}
    78  
    79  		try++
    80  
    81  		if err, ok := err.(*github.RateLimitError); ok {
    82  			waitDuration := defaultGithubSleep
    83  			until := time.Until(err.Rate.Reset.Time)
    84  			if until > 0 {
    85  				waitDuration = until
    86  			}
    87  			logrus.
    88  				WithField("err", err).
    89  				Infof("Hit the rate limit on try %d, sleeping for %s", try, waitDuration)
    90  			sleeper(waitDuration)
    91  			return true
    92  		}
    93  
    94  		if aerr, ok := err.(*github.AbuseRateLimitError); ok {
    95  			waitDuration := defaultGithubSleep
    96  			if d := aerr.RetryAfter; d != nil {
    97  				waitDuration = *d
    98  			}
    99  			logrus.
   100  				WithField("err", aerr).
   101  				Infof("Hit the abuse rate limit on try %d, sleeping for %s", try, waitDuration)
   102  			sleeper(waitDuration)
   103  			return true
   104  		}
   105  
   106  		if strings.Contains(err.Error(), "secondary rate limit. Please wait") {
   107  			rtime, err := rand.Int(rand.Reader, big.NewInt(30))
   108  			if err != nil {
   109  				logrus.Error(err)
   110  				return false
   111  			}
   112  			waitDuration := time.Duration(rtime.Int64()*int64(time.Second)) + defaultGithubSleep
   113  			logrus.
   114  				WithField("err", err).
   115  				Infof("Hit the GitHub secondary rate limit on try %d, sleeping for %s", try, waitDuration)
   116  			sleeper(waitDuration)
   117  			return true
   118  		}
   119  
   120  		return false
   121  	}
   122  }