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 }