github.com/smugmug/godynamo@v0.0.0-20151122084750-7913028f6623/authreq/authreq.go (about)

     1  // Implements the wrapper for versioned retryable DynamoDB requests.
     2  // See the init() function below for details about initial conf file processing.
     3  package authreq
     4  
     5  import (
     6  	"bytes"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"github.com/smugmug/godynamo/auth_v4"
    11  	"github.com/smugmug/godynamo/aws_const"
    12  	"github.com/smugmug/godynamo/conf"
    13  	ep "github.com/smugmug/godynamo/endpoint"
    14  	"log"
    15  	"math"
    16  	"math/rand"
    17  	"net/http"
    18  	"time"
    19  )
    20  
    21  const (
    22  	// auth version numbers
    23  	AUTH_V2 = 2
    24  	AUTH_V4 = 4
    25  )
    26  
    27  // Stipulate the current authorization version.
    28  const AUTH_VERSION = AUTH_V4
    29  
    30  var (
    31  	exceeded_msg_bytes, unrecognized_client_msg_bytes, throttling_msg_bytes []byte
    32  )
    33  
    34  func init() {
    35  	if AUTH_VERSION != AUTH_V4 {
    36  		panic("authreq: only v4 authentication is enabled")
    37  	}
    38  	// convert these to []byte so we can search within responses
    39  	exceeded_msg_bytes = []byte(aws_const.EXCEEDED_MSG)
    40  	unrecognized_client_msg_bytes = []byte(aws_const.UNRECOGNIZED_CLIENT_MSG)
    41  	throttling_msg_bytes = []byte(aws_const.THROTTLING_MSG)
    42  }
    43  
    44  // RetryReq_V4 sends a retry-able request using an ep.Endpoint structure and v4 auth.
    45  // Uses the global conf.
    46  func RetryReq_V4(v ep.Endpoint, amzTarget string) ([]byte, int, error) {
    47  	if !conf.IsValid(&conf.Vals) {
    48  		return nil, 0, errors.New("authreq.RetryReq_V4: conf not valid")
    49  	}
    50  	reqJSON, json_err := json.Marshal(v)
    51  	if json_err != nil {
    52  		return nil, 0, json_err
    53  	}
    54  	return retryReq(reqJSON, amzTarget, &conf.Vals)
    55  }
    56  
    57  // RetryReq_V4 sends a retry-able request using a JSON serialized request and v4 auth.
    58  // Uses the global conf.
    59  func RetryReqJSON_V4(reqJSON []byte, amzTarget string) ([]byte, int, error) {
    60  	if !conf.IsValid(&conf.Vals) {
    61  		return nil, 0, errors.New("authreq.RetryReqJSON_V4: conf not valid")
    62  	}
    63  	return retryReq(reqJSON, amzTarget, &conf.Vals)
    64  }
    65  
    66  // RetryReq_V4 sends a retry-able request using an ep.Endpoint structure and v4 auth.
    67  // Uses a parameterized conf.
    68  func RetryReq_V4WithConf(v ep.Endpoint, amzTarget string, c *conf.AWS_Conf) ([]byte, int, error) {
    69  	if !conf.IsValid(c) {
    70  		return nil, 0, errors.New("authreq.RetryReqV4WithConf: conf not valid")
    71  	}
    72  	reqJSON, json_err := json.Marshal(v)
    73  	if json_err != nil {
    74  		return nil, 0, json_err
    75  	}
    76  	return retryReq(reqJSON, amzTarget, c)
    77  }
    78  
    79  // RetryReq_V4 sends a retry-able request using a JSON serialized request and v4 auth.
    80  // Uses a parameterized conf.
    81  func RetryReqJSON_V4WithConf(reqJSON []byte, amzTarget string, c *conf.AWS_Conf) ([]byte, int, error) {
    82  	if !conf.IsValid(c) {
    83  		return nil, 0, errors.New("authreq.RetryReqJSON_V4WithConf: conf not valid")
    84  	}
    85  	return retryReq(reqJSON, amzTarget, c)
    86  }
    87  
    88  // Implement exponential backoff for the req above in the case of 5xx errors
    89  // from aws. Algorithm is lifted from AWS docs.
    90  // returns []byte respBody, int httpcode, error
    91  func retryReq(reqJSON []byte, amzTarget string, c *conf.AWS_Conf) ([]byte, int, error) {
    92  	// conf.IsValid has already been established by caller
    93  	resp_body, amz_requestid, code, resp_err := auth_v4.ReqWithConf(reqJSON, amzTarget, c)
    94  	shouldRetry := false
    95  	if resp_err != nil {
    96  		e := fmt.Sprintf("authreq.retryReq:0 "+
    97  			" try AuthReq Fail:%s (reqid:%s)", resp_err.Error(), amz_requestid)
    98  		log.Printf("authreq.retryReq: call err %s\n", e)
    99  		shouldRetry = true
   100  	}
   101  	// see:
   102  	// http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ErrorHandling.html
   103  	if code >= http.StatusInternalServerError {
   104  		shouldRetry = true // all 5xx codes are deemed retryable by amazon
   105  	}
   106  	if code == http.StatusBadRequest {
   107  		if bytes.Contains(resp_body, exceeded_msg_bytes) {
   108  			log.Printf("authreq.retryReq THROUGHPUT WARNING RETRY\n")
   109  			shouldRetry = true
   110  		} else if bytes.Contains(resp_body, unrecognized_client_msg_bytes) {
   111  			log.Printf("authreq.retryReq CLIENT WARNING RETRY\n")
   112  			shouldRetry = true
   113  		} else if bytes.Contains(resp_body, throttling_msg_bytes) {
   114  			log.Printf("authreq.retryReq THROUGHPUT WARNING RETRY\n")
   115  			shouldRetry = true
   116  		} else {
   117  			log.Printf("authreq.retryReq un-retryable err: %s\n%s (reqid:%s)\n",
   118  				string(resp_body), string(reqJSON), amz_requestid)
   119  			shouldRetry = false
   120  		}
   121  	}
   122  	if !shouldRetry {
   123  		// not retryable
   124  		return resp_body, code, resp_err
   125  	} else {
   126  		// retry the request RETRIES time in the case of a 5xx
   127  		// response, with an exponentially decayed sleep interval
   128  
   129  		// seed our rand number generator g
   130  		g := rand.New(rand.NewSource(time.Now().UnixNano()))
   131  		for i := 1; i < aws_const.RETRIES; i++ {
   132  			// get random delay from range
   133  			// [0..4**i*100 ms)
   134  			log.Printf("authreq.retryReq: BEGIN SLEEP %v (code:%v) (REQ:%s) (reqid:%s)",
   135  				time.Now(), code, string(reqJSON), amz_requestid)
   136  			r := time.Millisecond *
   137  				time.Duration(g.Int63n(int64(
   138  					math.Pow(4, float64(i)))*
   139  					100))
   140  			time.Sleep(r)
   141  			log.Printf("authreq.retryReq END SLEEP %v\n", time.Now())
   142  			shouldRetry = false
   143  			resp_body, amz_requestid, code, resp_err := auth_v4.ReqWithConf(reqJSON, amzTarget, c)
   144  			if resp_err != nil {
   145  				_ = fmt.Sprintf("authreq.retryReq:1 "+
   146  					" try AuthReq Fail:%s (reqid:%s)", resp_err.Error(), amz_requestid)
   147  				shouldRetry = true
   148  			}
   149  			if code >= http.StatusInternalServerError {
   150  				shouldRetry = true
   151  			}
   152  			if code == http.StatusBadRequest {
   153  				if bytes.Contains(resp_body, exceeded_msg_bytes) {
   154  					log.Printf("authreq.retryReq THROUGHPUT WARNING RETRY\n")
   155  					shouldRetry = true
   156  				}
   157  			}
   158  			if !shouldRetry {
   159  				// worked! no need to retry
   160  				log.Printf("authreq.retryReq RETRY LOOP SUCCESS")
   161  				return resp_body, code, resp_err
   162  			}
   163  		}
   164  		e := fmt.Sprintf("authreq.retryReq: failed retries on %s:%s",
   165  			amzTarget, string(reqJSON))
   166  		return nil, 0, errors.New(e)
   167  	}
   168  }