github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/httputil/retry.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2016 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package httputil
    21  
    22  import (
    23  	"fmt"
    24  	"io"
    25  	"net"
    26  	"net/http"
    27  	"net/url"
    28  	"os"
    29  	"strings"
    30  	"syscall"
    31  	"time"
    32  
    33  	"gopkg.in/retry.v1"
    34  
    35  	"github.com/snapcore/snapd/logger"
    36  	"github.com/snapcore/snapd/osutil"
    37  )
    38  
    39  type PersistentNetworkError struct {
    40  	Err error
    41  }
    42  
    43  func (e *PersistentNetworkError) Error() string {
    44  	return fmt.Sprintf("persistent network error: %v", e.Err)
    45  }
    46  
    47  func MaybeLogRetryAttempt(url string, attempt *retry.Attempt, startTime time.Time) {
    48  	if osutil.GetenvBool("SNAPD_DEBUG") || attempt.Count() > 1 {
    49  		logger.Debugf("Retrying %s, attempt %d, elapsed time=%v", url, attempt.Count(), time.Since(startTime))
    50  	}
    51  }
    52  
    53  func maybeLogRetrySummary(startTime time.Time, url string, attempt *retry.Attempt, resp *http.Response, err error) {
    54  	if osutil.GetenvBool("SNAPD_DEBUG") || attempt.Count() > 1 {
    55  		var status string
    56  		if err != nil {
    57  			status = err.Error()
    58  		} else if resp != nil {
    59  			status = fmt.Sprintf("%d", resp.StatusCode)
    60  		}
    61  		logger.Debugf("The retry loop for %s finished after %d retries, elapsed time=%v, status: %s", url, attempt.Count(), time.Since(startTime), status)
    62  	}
    63  }
    64  
    65  func ShouldRetryHttpResponse(attempt *retry.Attempt, resp *http.Response) bool {
    66  	if !attempt.More() {
    67  		return false
    68  	}
    69  	return resp.StatusCode >= 500
    70  }
    71  
    72  // isHttp2ProtocolError returns true if the given error is a http2
    73  // stream error with code 0x1 (PROTOCOL_ERROR).
    74  //
    75  // Unfortunately it seems this is not easy to detect. In e3be142 this
    76  // code tried to be smart and detect this via http2.StreamError but it
    77  // seems like with the h2_bundle.go in the go distro this does not
    78  // work, i.e. in https://travis-ci.org/snapcore/snapd/jobs/575471665
    79  // we still got protocol errors even with this detection code.
    80  //
    81  // So this code falls back to simple and naive detection.
    82  func isHttp2ProtocolError(err error) bool {
    83  	if strings.Contains(err.Error(), "PROTOCOL_ERROR") {
    84  		return true
    85  	}
    86  	// here is what a protocol error may look like:
    87  	// "DEBUG: Not retrying: http.http2StreamError{StreamID:0x1, Code:0x1, Cause:error(nil)}"
    88  	if strings.Contains(err.Error(), "http2StreamError") && strings.Contains(err.Error(), "Code:0x1,") {
    89  		return true
    90  	}
    91  	return false
    92  }
    93  
    94  func ShouldRetryAttempt(attempt *retry.Attempt, err error) bool {
    95  	if !attempt.More() {
    96  		return false
    97  	}
    98  	return ShouldRetryError(err)
    99  }
   100  
   101  // ShouldRetryError returns true for transient network errors like when
   102  // the remote side returns a connection reset and it's sensible to retry
   103  // after a short time.
   104  //
   105  // XXX: Note that currently also NoNetwork(err) errors are reported
   106  // with true here.
   107  func ShouldRetryError(err error) (b bool) {
   108  	if err == nil {
   109  		return false
   110  	}
   111  	defer func() {
   112  		logger.Debugf("ShouldRetryError: %v %T -> %v", err, err, b)
   113  	}()
   114  
   115  	if urlErr, ok := err.(*url.Error); ok {
   116  		err = urlErr.Err
   117  	}
   118  	if netErr, ok := err.(net.Error); ok {
   119  		if netErr.Timeout() {
   120  			logger.Debugf("Retrying because of: %s", netErr)
   121  			return true
   122  		}
   123  	}
   124  	// The CDN sometimes resets the connection (LP:#1617765), also
   125  	// retry in this case
   126  	if opErr, ok := err.(*net.OpError); ok {
   127  		// "no such host" is a permanent error and should not be retried.
   128  		if opErr.Op == "dial" && strings.Contains(opErr.Error(), "no such host") {
   129  			return false
   130  		}
   131  		// peeling the onion
   132  		if syscallErr, ok := opErr.Err.(*os.SyscallError); ok {
   133  			if syscallErr.Err == syscall.ECONNRESET {
   134  				logger.Debugf("Retrying because of: %s", opErr)
   135  				return true
   136  			}
   137  			// FIXME: code below is not (unit) tested and
   138  			// it is unclear if we need it with the new
   139  			// opErr.Temporary() "if" below
   140  			if opErr.Op == "dial" {
   141  				logger.Debugf("Retrying because of: %#v (syscall error: %#v)", opErr, syscallErr.Err)
   142  				return true
   143  			}
   144  			logger.Debugf("Encountered syscall error: %#v", syscallErr)
   145  		}
   146  
   147  		// If we are unable to talk to a DNS go1.9+ will set
   148  		// opErr.IsTemporary - we also support go1.6 so we need to
   149  		// add a workaround here. This block can go away once we
   150  		// use go1.9+ only.
   151  		if dnsErr, ok := opErr.Err.(*net.DNSError); ok {
   152  			// The horror, the horror
   153  			// TODO: stop Arch to use the cgo resolver
   154  			// which requires the right side of the OR
   155  			if strings.Contains(dnsErr.Err, "connection refused") || strings.Contains(dnsErr.Err, "Temporary failure in name resolution") {
   156  				logger.Debugf("Retrying because of temporary net error (DNS): %#v", dnsErr)
   157  				return true
   158  			}
   159  		}
   160  
   161  		// Retry for temporary network errors (like dns errors in 1.9+)
   162  		if opErr.Temporary() {
   163  			logger.Debugf("Retrying because of temporary net error: %#v", opErr)
   164  			return true
   165  		}
   166  		logger.Debugf("Encountered non temporary net.OpError: %#v", opErr)
   167  	}
   168  
   169  	// we see this from http2 downloads sometimes - it is unclear what
   170  	// is causing it but https://github.com/golang/go/issues/29125
   171  	// indicates a retry might be enough. Note that we get the
   172  	// PROTOCOL_ERROR *from* the remote side (fastly it seems)
   173  	if isHttp2ProtocolError(err) {
   174  		logger.Debugf("Retrying because of: %s", err)
   175  		return true
   176  	}
   177  
   178  	if err == io.ErrUnexpectedEOF || err == io.EOF {
   179  		logger.Debugf("Retrying because of: %s (%s)", err, err)
   180  		return true
   181  	}
   182  
   183  	if osutil.GetenvBool("SNAPD_DEBUG") {
   184  		logger.Debugf("Not retrying: %#v", err)
   185  	}
   186  
   187  	return false
   188  }
   189  
   190  // NoNetwork returns true if the error indicates that there is no network
   191  // connection available, i.e. network unreachable or down or DNS unavailable.
   192  func NoNetwork(err error) (b bool) {
   193  	defer func() {
   194  		logger.Debugf("NoNetwork: %v %T -> %v", err, err, b)
   195  	}()
   196  
   197  	return isNetworkDown(err) || isDnsUnavailable(err)
   198  }
   199  
   200  func isNetworkDown(err error) bool {
   201  	if err == nil {
   202  		return false
   203  	}
   204  	urlErr, ok := err.(*url.Error)
   205  	if !ok {
   206  		return false
   207  	}
   208  	opErr, ok := urlErr.Err.(*net.OpError)
   209  	if !ok {
   210  		return false
   211  	}
   212  
   213  	switch lowerErr := opErr.Err.(type) {
   214  	case *net.DNSError:
   215  		// on 16.04 we will not have SyscallError here, but DNSError, with
   216  		// no further details other than error message
   217  		return strings.Contains(lowerErr.Err, "connect: network is unreachable")
   218  	case *os.SyscallError:
   219  		if errnoErr, ok := lowerErr.Err.(syscall.Errno); ok {
   220  			// the errno codes from kernel/libc when the network is down
   221  			return errnoErr == syscall.ENETUNREACH || errnoErr == syscall.ENETDOWN
   222  		}
   223  	}
   224  	return false
   225  }
   226  
   227  func isDnsUnavailable(err error) bool {
   228  	if err == nil {
   229  		return false
   230  	}
   231  
   232  	urlErr, ok := err.(*url.Error)
   233  	if !ok {
   234  		return false
   235  	}
   236  	opErr, ok := urlErr.Err.(*net.OpError)
   237  	if !ok {
   238  		return false
   239  	}
   240  
   241  	dnsErr, ok := opErr.Err.(*net.DNSError)
   242  	if !ok {
   243  		return false
   244  	}
   245  
   246  	// We really want to check for EAI_AGAIN error here - but this is
   247  	// not exposed in net.DNSError and in go-1.10 it is not even
   248  	// a temporary error so there is no way to distiguish it other
   249  	// than a fugly string compare on a (potentially) localized string
   250  	return strings.Contains(dnsErr.Err, "Temporary failure in name resolution")
   251  }
   252  
   253  // RetryRequest calls doRequest and read the response body in a retry loop using the given retryStrategy.
   254  func RetryRequest(endpoint string, doRequest func() (*http.Response, error), readResponseBody func(resp *http.Response) error, retryStrategy retry.Strategy) (resp *http.Response, err error) {
   255  	var attempt *retry.Attempt
   256  	startTime := time.Now()
   257  	for attempt = retry.Start(retryStrategy, nil); attempt.Next(); {
   258  		MaybeLogRetryAttempt(endpoint, attempt, startTime)
   259  
   260  		resp, err = doRequest()
   261  		if err != nil {
   262  			if ShouldRetryAttempt(attempt, err) {
   263  				continue
   264  			}
   265  
   266  			if isNetworkDown(err) || isDnsUnavailable(err) {
   267  				err = &PersistentNetworkError{Err: err}
   268  			}
   269  			break
   270  		}
   271  
   272  		if ShouldRetryHttpResponse(attempt, resp) {
   273  			resp.Body.Close()
   274  			continue
   275  		} else {
   276  			err := readResponseBody(resp)
   277  			resp.Body.Close()
   278  			if err != nil {
   279  				if ShouldRetryAttempt(attempt, err) {
   280  					continue
   281  				} else {
   282  					maybeLogRetrySummary(startTime, endpoint, attempt, resp, err)
   283  					return nil, err
   284  				}
   285  			}
   286  		}
   287  		// break out from retry loop
   288  		break
   289  	}
   290  	maybeLogRetrySummary(startTime, endpoint, attempt, resp, err)
   291  
   292  	return resp, err
   293  }