github.com/searKing/golang/go@v1.2.74/net/http/backoff.go (about)

     1  // Copyright 2022 The searKing Author. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package http
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"crypto/x509"
    11  	"encoding/json"
    12  	"fmt"
    13  	"io"
    14  	"net/http"
    15  	"net/url"
    16  	"regexp"
    17  	"strconv"
    18  	"strings"
    19  	"time"
    20  
    21  	time_ "github.com/searKing/golang/go/time"
    22  )
    23  
    24  var (
    25  	// A regular expression to match the error returned by net/http when the
    26  	// configured number of redirects is exhausted. This error isn't typed
    27  	// specifically so we resort to matching on the error string.
    28  	redirectsErrorRe = regexp.MustCompile(`stopped after \d+ redirects\z`)
    29  
    30  	// A regular expression to match the error returned by net/http when the
    31  	// scheme specified in the URL is invalid. This error isn't typed
    32  	// specifically so we resort to matching on the error string.
    33  	schemeErrorRe = regexp.MustCompile(`unsupported protocol scheme`)
    34  )
    35  
    36  // RetryAfter tries to parse Retry-After response header when a http.StatusTooManyRequests
    37  // (HTTP Code 429) is found in the resp parameter. Hence, it will return the number of
    38  // seconds the server states it may be ready to process more requests from this client.
    39  // Don't retry if the error was due to too many redirects.
    40  // Don't retry if the error was due to an invalid protocol scheme.
    41  // Don't retry if the error was due to TLS cert verification failure.
    42  // Don't retry if the http's StatusCode is http.StatusNotImplemented.
    43  func RetryAfter(resp *http.Response, err error, defaultBackoff time.Duration) (backoff time.Duration, retry bool) {
    44  	backoff = defaultBackoff
    45  	if resp != nil {
    46  		if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable {
    47  			if sleepInSeconds, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64); err == nil {
    48  				backoff = time.Second * time.Duration(sleepInSeconds)
    49  			}
    50  		}
    51  	}
    52  
    53  	if err != nil {
    54  		if v, ok := err.(*url.Error); ok {
    55  			// Don't retry if the error was due to too many redirects.
    56  			if redirectsErrorRe.MatchString(v.Error()) {
    57  				return backoff, false
    58  			}
    59  
    60  			// Don't retry if the error was due to an invalid protocol scheme.
    61  			if schemeErrorRe.MatchString(v.Error()) {
    62  				return backoff, false
    63  			}
    64  
    65  			// Don't retry if the error was due to TLS cert verification failure.
    66  			if _, ok := v.Err.(x509.UnknownAuthorityError); ok {
    67  				return backoff, false
    68  			}
    69  		}
    70  
    71  		// The error is likely recoverable so retry.
    72  		return backoff, true
    73  	}
    74  
    75  	if resp != nil {
    76  		// 429 Too Many Requests is recoverable. Sometimes the server puts
    77  		// a Retry-After response header to indicate when the server is
    78  		// available to start processing request from client.
    79  		if resp.StatusCode == http.StatusTooManyRequests {
    80  			return backoff, true
    81  		}
    82  
    83  		// Check the response code. We retry on 500-range responses to allow
    84  		// the server time to recover, as 500's are typically not permanent
    85  		// errors and may relate to outages on the server side. This will catch
    86  		// invalid response codes as well, like 0 and 999.
    87  		if resp.StatusCode == 0 || (resp.StatusCode >= http.StatusInternalServerError && resp.StatusCode != http.StatusNotImplemented) {
    88  			return backoff, true
    89  		}
    90  	}
    91  	return backoff, false
    92  }
    93  
    94  // ReplaceHttpRequestBody replace Body and recalculate ContentLength
    95  // If ContentLength should not be recalculated, save and restore it after ReplaceHttpRequestBody
    96  func ReplaceHttpRequestBody(req *http.Request, body io.Reader) {
    97  	if req.Body != nil {
    98  		req.Body.Close()
    99  	}
   100  	rc, ok := body.(io.ReadCloser)
   101  	if !ok && body != nil {
   102  		rc = io.NopCloser(body)
   103  	}
   104  	req.Body = rc
   105  	req.ContentLength = 0
   106  	if body != nil {
   107  		switch v := body.(type) {
   108  		case *bytes.Buffer:
   109  			req.ContentLength = int64(v.Len())
   110  			buf := v.Bytes()
   111  			req.GetBody = func() (io.ReadCloser, error) {
   112  				r := bytes.NewReader(buf)
   113  				return io.NopCloser(r), nil
   114  			}
   115  		case *bytes.Reader:
   116  			req.ContentLength = int64(v.Len())
   117  			snapshot := *v
   118  			req.GetBody = func() (io.ReadCloser, error) {
   119  				r := snapshot
   120  				return io.NopCloser(&r), nil
   121  			}
   122  		case *strings.Reader:
   123  			req.ContentLength = int64(v.Len())
   124  			snapshot := *v
   125  			req.GetBody = func() (io.ReadCloser, error) {
   126  				r := snapshot
   127  				return io.NopCloser(&r), nil
   128  			}
   129  		default:
   130  			// This is where we'd set it to -1 (at least
   131  			// if body != NoBody) to mean unknown, but
   132  			// that broke people during the Go 1.8 testing
   133  			// period. People depend on it being 0 I
   134  			// guess. Maybe retry later. See Issue 18117.
   135  		}
   136  		// For client requests, Request.ContentLength of 0
   137  		// means either actually 0, or unknown. The only way
   138  		// to explicitly say that the ContentLength is zero is
   139  		// to set the Body to nil. But turns out too much code
   140  		// depends on NewRequest returning a non-nil Body,
   141  		// so we use a well-known ReadCloser variable instead
   142  		// and have the http package also treat that sentinel
   143  		// variable to mean explicitly zero.
   144  		if req.GetBody != nil && req.ContentLength == 0 {
   145  			req.Body = http.NoBody
   146  			req.GetBody = func() (io.ReadCloser, error) { return http.NoBody, nil }
   147  		}
   148  	}
   149  }
   150  
   151  // ClientInvoker is called by ClientInterceptor to complete RPCs.
   152  type ClientInvoker func(req *http.Request, retry int) (*http.Response, error)
   153  
   154  // ClientInterceptor intercepts the execution of a HTTP on the client.
   155  // interceptors can be specified as a DoWithBackoffOption, using
   156  // WithClientInterceptor() or WithChainClientInterceptor(), when DoWithBackoffOption.
   157  // When a interceptor(s) is set, gRPC delegates all http invocations to the interceptor,
   158  // and it is the responsibility of the interceptor to call invoker to complete the processing
   159  // of the HTTP.
   160  type ClientInterceptor func(req *http.Request, retry int, invoker ClientInvoker, opts ...DoWithBackoffOption) (resp *http.Response, err error)
   161  
   162  type RetryAfterHandler func(resp *http.Response, err error, defaultBackoff time.Duration) (backoff time.Duration, retry bool)
   163  
   164  // DoRetryHandler send an HTTP request with retry seq and returns an HTTP response, following
   165  // policy (such as redirects, cookies, auth) as configured on the
   166  // client.
   167  type DoRetryHandler = ClientInvoker
   168  
   169  var DefaultClientDoRetryHandler = func(req *http.Request, retry int) (*http.Response, error) {
   170  	return http.DefaultClient.Do(req)
   171  }
   172  
   173  var DefaultTransportDoRetryHandler = func(req *http.Request, retry int) (*http.Response, error) {
   174  	return http.DefaultTransport.RoundTrip(req)
   175  }
   176  
   177  //go:generate go-option -type "doWithBackoff"
   178  type doWithBackoff struct {
   179  	DoRetryHandler           DoRetryHandler
   180  	clientInterceptor        ClientInterceptor
   181  	ChainClientInterceptors  []ClientInterceptor
   182  	RetryAfter               RetryAfterHandler
   183  	ExponentialBackOffOption []time_.ExponentialBackOffOption
   184  }
   185  
   186  func (o *doWithBackoff) SetDefault() {
   187  	o.DoRetryHandler = DefaultClientDoRetryHandler
   188  	o.RetryAfter = RetryAfter
   189  }
   190  
   191  // getClientInvoker recursively generate the chained client invoker.
   192  func getClientInvoker(interceptors []ClientInterceptor, curr int, finalInvoker ClientInvoker, opts ...DoWithBackoffOption) ClientInvoker {
   193  	if curr == len(interceptors)-1 {
   194  		return finalInvoker
   195  	}
   196  	return func(req *http.Request, retry int) (*http.Response, error) {
   197  		return interceptors[curr+1](req, retry, getClientInvoker(interceptors, curr+1, finalInvoker), opts...)
   198  	}
   199  }
   200  
   201  func (o *doWithBackoff) Complete() {
   202  	if o.DoRetryHandler == nil {
   203  		o.DoRetryHandler = DefaultClientDoRetryHandler
   204  	}
   205  	interceptors := o.ChainClientInterceptors
   206  	o.ChainClientInterceptors = nil
   207  	// Prepend o.ClientInterceptor to the chaining interceptors if it exists, since ClientInterceptor will
   208  	// be executed before any other chained interceptors.
   209  	if o.clientInterceptor != nil {
   210  		interceptors = append([]ClientInterceptor{o.clientInterceptor}, interceptors...)
   211  	}
   212  	var chainedInt ClientInterceptor
   213  	if len(interceptors) == 0 {
   214  		chainedInt = nil
   215  	} else if len(interceptors) == 1 {
   216  		chainedInt = interceptors[0]
   217  	} else {
   218  		chainedInt = func(req *http.Request, retry int, invoker ClientInvoker, opts ...DoWithBackoffOption) (resp *http.Response, err error) {
   219  			return interceptors[0](req, retry, getClientInvoker(interceptors, 0, invoker), opts...)
   220  		}
   221  	}
   222  	o.clientInterceptor = chainedInt
   223  }
   224  
   225  // DoWithBackoff will retry by exponential backoff if failed.
   226  // If request is not rewindable, retry wil be skipped.
   227  func DoWithBackoff(httpReq *http.Request, opts ...DoWithBackoffOption) (*http.Response, error) {
   228  	var opt doWithBackoff
   229  	opt.SetDefault()
   230  	opt.ApplyOptions(opts...)
   231  	if opt.RetryAfter == nil {
   232  		opt.RetryAfter = RetryAfter
   233  	}
   234  	opt.Complete()
   235  
   236  	var option []time_.ExponentialBackOffOption
   237  	option = append(option, time_.WithExponentialBackOffOptionMaxElapsedCount(3))
   238  	option = append(option, opt.ExponentialBackOffOption...)
   239  	backoff := time_.NewDefaultExponentialBackOff(option...)
   240  	rewindableErr := RequestWithBodyRewindable(httpReq)
   241  	var retries int
   242  	for {
   243  		if retries > 0 && httpReq.GetBody != nil {
   244  			newBody, err := httpReq.GetBody()
   245  			if err != nil {
   246  				return nil, err
   247  			}
   248  			httpReq.Body = newBody
   249  		}
   250  		var do = opt.DoRetryHandler
   251  		httpDo := do
   252  		if opt.clientInterceptor != nil {
   253  			httpDo = func(req *http.Request, retry int) (*http.Response, error) {
   254  				return opt.clientInterceptor(req, retry, do, opts...)
   255  			}
   256  		}
   257  		resp, err := httpDo(httpReq, retries)
   258  
   259  		wait, ok := backoff.NextBackOff()
   260  		if !ok {
   261  			if err != nil {
   262  				return nil, fmt.Errorf("http do reach backoff limit after retries %d: %w", retries, err)
   263  			} else {
   264  				return resp, nil
   265  			}
   266  		}
   267  
   268  		wait, retry := opt.RetryAfter(resp, err, wait)
   269  		if !retry {
   270  			if err != nil {
   271  				return nil, fmt.Errorf("http do reach server limit after retries %d: %w", retries, err)
   272  			} else {
   273  				return resp, nil
   274  			}
   275  		}
   276  
   277  		if rewindableErr != nil {
   278  			if err != nil {
   279  				return nil, fmt.Errorf("http do cannot rewindbody after retries %d: %w", retries, err)
   280  			} else {
   281  				return resp, nil
   282  			}
   283  		}
   284  
   285  		timer := time.NewTimer(wait)
   286  		select {
   287  		case <-timer.C:
   288  			retries++
   289  			continue
   290  		case <-httpReq.Context().Done():
   291  			timer.Stop()
   292  			if err != nil {
   293  				return nil, fmt.Errorf("http do canceled after retries %d: %w", retries, err)
   294  			} else {
   295  				return resp, nil
   296  			}
   297  		}
   298  	}
   299  }
   300  
   301  func HeadWithBackoff(ctx context.Context, url string, opts ...DoWithBackoffOption) (*http.Response, error) {
   302  	req, err := http.NewRequest(http.MethodHead, url, nil)
   303  	req = req.WithContext(ctx)
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  	return DoWithBackoff(req, opts...)
   308  }
   309  
   310  func GetWithBackoff(ctx context.Context, url string, opts ...DoWithBackoffOption) (*http.Response, error) {
   311  	req, err := http.NewRequest(http.MethodGet, url, nil)
   312  	req = req.WithContext(ctx)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  	return DoWithBackoff(req, opts...)
   317  }
   318  
   319  func PostWithBackoff(ctx context.Context, url, contentType string, body io.Reader, opts ...DoWithBackoffOption) (resp *http.Response, err error) {
   320  	req, err := http.NewRequest(http.MethodPost, url, body)
   321  	req = req.WithContext(ctx)
   322  	if err != nil {
   323  		return nil, err
   324  	}
   325  	if contentType != "" {
   326  		req.Header.Set("Content-Type", contentType)
   327  	}
   328  	return DoWithBackoff(req, opts...)
   329  }
   330  
   331  func PostFormWithBackoff(ctx context.Context, url string, data url.Values, opts ...DoWithBackoffOption) (resp *http.Response, err error) {
   332  	return PostWithBackoff(ctx, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()), opts...)
   333  }
   334  
   335  func PutWithBackoff(ctx context.Context, url, contentType string, body io.Reader, opts ...DoWithBackoffOption) (resp *http.Response, err error) {
   336  	req, err := http.NewRequest(http.MethodPut, url, body)
   337  	req = req.WithContext(ctx)
   338  	if err != nil {
   339  		return nil, err
   340  	}
   341  	if contentType != "" {
   342  		req.Header.Set("Content-Type", contentType)
   343  	}
   344  	return DoWithBackoff(req, opts...)
   345  }
   346  
   347  // DoJson the same as HttpDo, but bind with json
   348  func DoJson(httpReq *http.Request, req, resp interface{}) error {
   349  	if req != nil {
   350  		data, err := json.Marshal(req)
   351  		if err != nil {
   352  			return err
   353  		}
   354  		reqBody := bytes.NewReader(data)
   355  		httpReq.Header.Set("Content-Type", "application/json")
   356  		ReplaceHttpRequestBody(httpReq, reqBody)
   357  	}
   358  
   359  	httpResp, err := DefaultClientDoRetryHandler(httpReq, 0)
   360  	if err != nil {
   361  		return err
   362  	}
   363  	defer httpResp.Body.Close()
   364  	if resp == nil {
   365  		return nil
   366  	}
   367  
   368  	body, err := io.ReadAll(httpResp.Body)
   369  	if err != nil {
   370  		return err
   371  	}
   372  
   373  	return json.Unmarshal(body, resp)
   374  }
   375  
   376  // DoJsonWithBackoff the same as DoWithBackoff, but bind with json
   377  func DoJsonWithBackoff(httpReq *http.Request, req, resp interface{}, opts ...DoWithBackoffOption) error {
   378  	if req != nil {
   379  		data, err := json.Marshal(req)
   380  		if err != nil {
   381  			return err
   382  		}
   383  		reqBody := bytes.NewReader(data)
   384  		httpReq.Header.Set("Content-Type", "application/json")
   385  		ReplaceHttpRequestBody(httpReq, reqBody)
   386  	}
   387  	httpResp, err := DoWithBackoff(httpReq, opts...)
   388  
   389  	if err != nil {
   390  		return err
   391  	}
   392  	defer httpResp.Body.Close()
   393  	if resp == nil {
   394  		return nil
   395  	}
   396  
   397  	body, err := io.ReadAll(httpResp.Body)
   398  	if err != nil {
   399  		return err
   400  	}
   401  	return json.Unmarshal(body, resp)
   402  }