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