github.com/snowflakedb/gosnowflake@v1.9.0/retry_test.go (about) 1 // Copyright (c) 2017-2022 Snowflake Computing Inc. All rights reserved. 2 3 package gosnowflake 4 5 import ( 6 "bytes" 7 "context" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "strconv" 13 "strings" 14 "testing" 15 "time" 16 ) 17 18 func fakeRequestFunc(_, _ string, _ io.Reader) (*http.Request, error) { 19 return nil, nil 20 } 21 22 func emptyRequest(method string, urlStr string, body io.Reader) (*http.Request, error) { 23 return http.NewRequest(method, urlStr, body) 24 } 25 26 type fakeHTTPError struct { 27 err string 28 timeout bool 29 } 30 31 func (e *fakeHTTPError) Error() string { return e.err } 32 func (e *fakeHTTPError) Timeout() bool { return e.timeout } 33 func (e *fakeHTTPError) Temporary() bool { return true } 34 35 type fakeResponseBody struct { 36 body []byte 37 cnt int 38 } 39 40 func (b *fakeResponseBody) Read(p []byte) (n int, err error) { 41 if b.cnt == 0 { 42 copy(p, b.body) 43 b.cnt = 1 44 return len(b.body), nil 45 } 46 b.cnt = 0 47 return 0, io.EOF 48 } 49 50 func (b *fakeResponseBody) Close() error { 51 return nil 52 } 53 54 type fakeHTTPClient struct { 55 t *testing.T // for assertions 56 cnt int // number of retry 57 success bool // return success after retry in cnt times 58 timeout bool // timeout 59 body []byte // return body 60 reqBody []byte // last request body 61 statusCode int // status code 62 retryNumber int // consecutive number of retries 63 expectedQueryParams map[int]map[string]string // expected query params per each retry (0-based) 64 } 65 66 func (c *fakeHTTPClient) Do(req *http.Request) (*http.Response, error) { 67 defer func() { 68 c.retryNumber++ 69 }() 70 if req != nil { 71 buf := new(bytes.Buffer) 72 buf.ReadFrom(req.Body) 73 c.reqBody = buf.Bytes() 74 } 75 76 if len(c.expectedQueryParams) > 0 { 77 expectedQueryParams, ok := c.expectedQueryParams[c.retryNumber] 78 if ok { 79 for queryParamName, expectedValue := range expectedQueryParams { 80 actualValue := req.URL.Query().Get(queryParamName) 81 if actualValue != expectedValue { 82 c.t.Fatalf("expected query param %v to be %v, got %v", queryParamName, expectedValue, actualValue) 83 } 84 } 85 } 86 } 87 88 c.cnt-- 89 if c.cnt < 0 { 90 c.cnt = 0 91 } 92 logger.Infof("fakeHTTPClient.cnt: %v", c.cnt) 93 94 var retcode int 95 if c.success && c.cnt == 0 { 96 retcode = 200 97 } else { 98 if c.timeout { 99 // simulate timeout 100 time.Sleep(time.Second * 1) 101 return nil, &fakeHTTPError{ 102 err: "Whatever reason (Client.Timeout exceeded while awaiting headers)", 103 timeout: true, 104 } 105 } 106 if c.statusCode != 0 { 107 retcode = c.statusCode 108 } else { 109 retcode = 0 110 } 111 } 112 113 ret := &http.Response{ 114 StatusCode: retcode, 115 Body: &fakeResponseBody{body: c.body}, 116 } 117 return ret, nil 118 } 119 120 func TestRequestGUID(t *testing.T) { 121 var ridReplacer requestGUIDReplacer 122 var testURL *url.URL 123 var actualURL *url.URL 124 retryTime := 4 125 126 // empty url 127 testURL = &url.URL{} 128 ridReplacer = newRequestGUIDReplace(testURL) 129 for i := 0; i < retryTime; i++ { 130 actualURL = ridReplacer.replace() 131 if actualURL.String() != "" { 132 t.Fatalf("empty url not replaced by an empty one, got %s", actualURL) 133 } 134 } 135 136 // url with on retry id 137 testURL = &url.URL{ 138 Path: "/" + requestIDKey + "=123-1923-9?param2=value", 139 } 140 ridReplacer = newRequestGUIDReplace(testURL) 141 for i := 0; i < retryTime; i++ { 142 actualURL = ridReplacer.replace() 143 144 if actualURL != testURL { 145 t.Fatalf("url without retry id not replaced by origin one, got %s", actualURL) 146 } 147 } 148 149 // url with retry id 150 // With both prefix and suffix 151 prefix := "/" + requestIDKey + "=123-1923-9?" + requestGUIDKey + "=" 152 suffix := "?param2=value" 153 testURL = &url.URL{ 154 Path: prefix + "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + suffix, 155 } 156 ridReplacer = newRequestGUIDReplace(testURL) 157 for i := 0; i < retryTime; i++ { 158 actualURL = ridReplacer.replace() 159 if (!strings.HasPrefix(actualURL.Path, prefix)) || 160 (!strings.HasSuffix(actualURL.Path, suffix)) || 161 len(testURL.Path) != len(actualURL.Path) { 162 t.Fatalf("Retry url not replaced correctedly: \n origin: %s \n result: %s", testURL, actualURL) 163 } 164 } 165 166 // With no suffix 167 prefix = "/" + requestIDKey + "=123-1923-9?" + requestGUIDKey + "=" 168 suffix = "" 169 testURL = &url.URL{ 170 Path: prefix + "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + suffix, 171 } 172 ridReplacer = newRequestGUIDReplace(testURL) 173 for i := 0; i < retryTime; i++ { 174 actualURL = ridReplacer.replace() 175 if (!strings.HasPrefix(actualURL.Path, prefix)) || 176 (!strings.HasSuffix(actualURL.Path, suffix)) || 177 len(testURL.Path) != len(actualURL.Path) { 178 t.Fatalf("Retry url not replaced correctedly: \n origin: %s \n result: %s", testURL, actualURL) 179 } 180 181 } 182 // With no prefix 183 prefix = requestGUIDKey + "=" 184 suffix = "?param2=value" 185 testURL = &url.URL{ 186 Path: prefix + "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + suffix, 187 } 188 ridReplacer = newRequestGUIDReplace(testURL) 189 for i := 0; i < retryTime; i++ { 190 actualURL = ridReplacer.replace() 191 if (!strings.HasPrefix(actualURL.Path, prefix)) || 192 (!strings.HasSuffix(actualURL.Path, suffix)) || 193 len(testURL.Path) != len(actualURL.Path) { 194 t.Fatalf("Retry url not replaced correctedly: \n origin: %s \n result: %s", testURL, actualURL) 195 } 196 } 197 } 198 199 func TestRetryQuerySuccess(t *testing.T) { 200 logger.Info("Retry N times and Success") 201 client := &fakeHTTPClient{ 202 cnt: 3, 203 success: true, 204 statusCode: 429, 205 expectedQueryParams: map[int]map[string]string{ 206 0: { 207 "retryCount": "", 208 "retryReason": "", 209 "clientStartTime": "", 210 }, 211 1: { 212 "retryCount": "1", 213 "retryReason": "429", 214 "clientStartTime": "123456", 215 }, 216 2: { 217 "retryCount": "2", 218 "retryReason": "429", 219 "clientStartTime": "123456", 220 }, 221 }, 222 t: t, 223 } 224 urlPtr, err := url.Parse("https://fakeaccountretrysuccess.snowflakecomputing.com:443/queries/v1/query-request?" + requestIDKey + "=testid") 225 assertNilF(t, err, "failed to parse the test URL") 226 _, err = newRetryHTTP(context.Background(), 227 client, 228 emptyRequest, urlPtr, make(map[string]string), 60*time.Second, 3, constTimeProvider(123456), &Config{IncludeRetryReason: ConfigBoolTrue}).doPost().setBody([]byte{0}).execute() 229 assertNilF(t, err, "failed to run retry") 230 var values url.Values 231 values, err = url.ParseQuery(urlPtr.RawQuery) 232 assertNilF(t, err, "failed to parse the test URL") 233 retry, err := strconv.Atoi(values.Get(retryCountKey)) 234 if err != nil { 235 t.Fatalf("failed to get retry counter: %v", err) 236 } 237 if retry < 2 { 238 t.Fatalf("not enough retry counter: %v", retry) 239 } 240 } 241 242 func TestRetryQuerySuccessWithRetryReasonDisabled(t *testing.T) { 243 logger.Info("Retry N times and Success") 244 client := &fakeHTTPClient{ 245 cnt: 3, 246 success: true, 247 statusCode: 429, 248 expectedQueryParams: map[int]map[string]string{ 249 0: { 250 "retryCount": "", 251 "retryReason": "", 252 "clientStartTime": "", 253 }, 254 1: { 255 "retryCount": "1", 256 "retryReason": "", 257 "clientStartTime": "123456", 258 }, 259 2: { 260 "retryCount": "2", 261 "retryReason": "", 262 "clientStartTime": "123456", 263 }, 264 }, 265 t: t, 266 } 267 urlPtr, err := url.Parse("https://fakeaccountretrysuccess.snowflakecomputing.com:443/queries/v1/query-request?" + requestIDKey + "=testid") 268 assertNilF(t, err, "failed to parse the test URL") 269 _, err = newRetryHTTP(context.Background(), 270 client, 271 emptyRequest, urlPtr, make(map[string]string), 60*time.Second, 3, constTimeProvider(123456), &Config{IncludeRetryReason: ConfigBoolFalse}).doPost().setBody([]byte{0}).execute() 272 assertNilF(t, err, "failed to run retry") 273 var values url.Values 274 values, err = url.ParseQuery(urlPtr.RawQuery) 275 assertNilF(t, err, "failed to parse the test URL") 276 retry, err := strconv.Atoi(values.Get(retryCountKey)) 277 if err != nil { 278 t.Fatalf("failed to get retry counter: %v", err) 279 } 280 if retry < 2 { 281 t.Fatalf("not enough retry counter: %v", retry) 282 } 283 } 284 285 func TestRetryQuerySuccessWithTimeout(t *testing.T) { 286 logger.Info("Retry N times and Success") 287 client := &fakeHTTPClient{ 288 cnt: 3, 289 success: true, 290 timeout: true, 291 expectedQueryParams: map[int]map[string]string{ 292 0: { 293 "retryCount": "", 294 "retryReason": "", 295 }, 296 1: { 297 "retryCount": "1", 298 "retryReason": "0", 299 }, 300 2: { 301 "retryCount": "2", 302 "retryReason": "0", 303 }, 304 }, 305 t: t, 306 } 307 urlPtr, err := url.Parse("https://fakeaccountretrysuccess.snowflakecomputing.com:443/queries/v1/query-request?" + requestIDKey + "=testid") 308 assertNilF(t, err, "failed to parse the test URL") 309 _, err = newRetryHTTP(context.Background(), 310 client, 311 emptyRequest, urlPtr, make(map[string]string), 60*time.Second, 3, constTimeProvider(123456), nil).doPost().setBody([]byte{0}).execute() 312 assertNilF(t, err, "failed to run retry") 313 var values url.Values 314 values, err = url.ParseQuery(urlPtr.RawQuery) 315 assertNilF(t, err, "failed to parse the test URL") 316 retry, err := strconv.Atoi(values.Get(retryCountKey)) 317 if err != nil { 318 t.Fatalf("failed to get retry counter: %v", err) 319 } 320 if retry < 2 { 321 t.Fatalf("not enough retry counter: %v", retry) 322 } 323 } 324 325 func TestRetryQueryFailWithTimeout(t *testing.T) { 326 logger.Info("Retry N times until there is a timeout and Fail") 327 client := &fakeHTTPClient{ 328 statusCode: http.StatusTooManyRequests, 329 success: false, 330 } 331 urlPtr, err := url.Parse("https://fakeaccountretryfail.snowflakecomputing.com:443/queries/v1/query-request?" + requestIDKey) 332 assertNilF(t, err, "failed to parse the test URL") 333 _, err = newRetryHTTP(context.Background(), 334 client, 335 emptyRequest, urlPtr, make(map[string]string), 20*time.Second, 100, defaultTimeProvider, nil).doPost().setBody([]byte{0}).execute() 336 assertNotNilF(t, err, "should fail to run retry") 337 var values url.Values 338 values, err = url.ParseQuery(urlPtr.RawQuery) 339 assertNilF(t, err, fmt.Sprintf("failed to parse the URL: %v", err)) 340 retry, err := strconv.Atoi(values.Get(retryCountKey)) 341 assertNilF(t, err, fmt.Sprintf("failed to get retry counter: %v", err)) 342 if retry < 2 { 343 t.Fatalf("not enough retries: %v", retry) 344 } 345 } 346 347 func TestRetryQueryFailWithMaxRetryCount(t *testing.T) { 348 maxRetryCount := 3 349 logger.Info("Retry 3 times until retry reaches MaxRetryCount and Fail") 350 client := &fakeHTTPClient{ 351 statusCode: http.StatusTooManyRequests, 352 success: false, 353 } 354 urlPtr, err := url.Parse("https://fakeaccountretryfail.snowflakecomputing.com:443/queries/v1/query-request?" + requestIDKey) 355 assertNilF(t, err, "failed to parse the test URL") 356 _, err = newRetryHTTP(context.Background(), 357 client, 358 emptyRequest, urlPtr, make(map[string]string), 15*time.Hour, maxRetryCount, defaultTimeProvider, nil).doPost().setBody([]byte{0}).execute() 359 assertNotNilF(t, err, "should fail to run retry") 360 var values url.Values 361 values, err = url.ParseQuery(urlPtr.RawQuery) 362 if err != nil { 363 t.Fatalf("failed to parse the URL: %v", err) 364 } 365 retryCount, err := strconv.Atoi(values.Get(retryCountKey)) 366 if err != nil { 367 t.Fatalf("failed to get retry counter: %v", err) 368 } 369 if retryCount < 3 { 370 t.Fatalf("not enough retries: %v; expected %v", retryCount, maxRetryCount) 371 } 372 } 373 374 func TestRetryLoginRequest(t *testing.T) { 375 logger.Info("Retry N times for timeouts and Success") 376 client := &fakeHTTPClient{ 377 cnt: 3, 378 success: true, 379 timeout: true, 380 t: t, 381 expectedQueryParams: map[int]map[string]string{ 382 0: { 383 "retryCount": "", 384 "retryReason": "", 385 }, 386 1: { 387 "retryCount": "", 388 "retryReason": "", 389 }, 390 2: { 391 "retryCount": "", 392 "retryReason": "", 393 }, 394 }, 395 } 396 urlPtr, err := url.Parse("https://fakeaccountretrylogin.snowflakecomputing.com:443/login-request?request_id=testid") 397 assertNilF(t, err, "failed to parse the test URL") 398 _, err = newRetryHTTP(context.Background(), 399 client, 400 emptyRequest, urlPtr, make(map[string]string), 60*time.Second, 3, defaultTimeProvider, nil).doPost().setBody([]byte{0}).execute() 401 assertNilF(t, err, "failed to run retry") 402 var values url.Values 403 values, err = url.ParseQuery(urlPtr.RawQuery) 404 assertNilF(t, err, "failed to parse the test URL") 405 if values.Get(retryCountKey) != "" { 406 t.Fatalf("no retry counter should be attached: %v", retryCountKey) 407 } 408 logger.Info("Retry N times for timeouts and Fail") 409 client = &fakeHTTPClient{ 410 success: false, 411 timeout: true, 412 } 413 _, err = newRetryHTTP(context.Background(), 414 client, 415 emptyRequest, urlPtr, make(map[string]string), 5*time.Second, 3, defaultTimeProvider, nil).doPost().setBody([]byte{0}).execute() 416 assertNotNilF(t, err, "should fail to run retry") 417 values, err = url.ParseQuery(urlPtr.RawQuery) 418 if err != nil { 419 t.Fatalf("failed to parse the URL: %v", err) 420 } 421 if values.Get(retryCountKey) != "" { 422 t.Fatalf("no retry counter should be attached: %v", retryCountKey) 423 } 424 } 425 426 func TestRetryAuthLoginRequest(t *testing.T) { 427 logger.Info("Retry N times always with newer body") 428 client := &fakeHTTPClient{ 429 cnt: 3, 430 success: true, 431 timeout: true, 432 } 433 urlPtr, err := url.Parse("https://fakeaccountretrylogin.snowflakecomputing.com:443/login-request?request_id=testid") 434 assertNilF(t, err, "failed to parse the test URL") 435 execID := 0 436 bodyCreator := func() ([]byte, error) { 437 execID++ 438 return []byte(fmt.Sprintf("execID: %d", execID)), nil 439 } 440 _, err = newRetryHTTP(context.Background(), 441 client, 442 http.NewRequest, urlPtr, make(map[string]string), 60*time.Second, 3, defaultTimeProvider, nil).doPost().setBodyCreator(bodyCreator).execute() 443 assertNilF(t, err, "failed to run retry") 444 if lastReqBody := string(client.reqBody); lastReqBody != "execID: 3" { 445 t.Fatalf("body should be updated on each request, expected: execID: 3, last body: %v", lastReqBody) 446 } 447 } 448 449 func TestLoginRetry429(t *testing.T) { 450 client := &fakeHTTPClient{ 451 cnt: 3, 452 success: true, 453 statusCode: 429, 454 } 455 urlPtr, err := url.Parse("https://fakeaccountretrylogin.snowflakecomputing.com:443/login-request?request_id=testid") 456 assertNilF(t, err, "failed to parse the test URL") 457 458 _, err = newRetryHTTP(context.Background(), 459 client, 460 emptyRequest, urlPtr, make(map[string]string), 60*time.Second, 3, defaultTimeProvider, nil).doPost().setBody([]byte{0}).execute() // enable doRaise4XXX 461 assertNilF(t, err, "failed to run retry") 462 463 var values url.Values 464 values, err = url.ParseQuery(urlPtr.RawQuery) 465 assertNilF(t, err, fmt.Sprintf("failed to parse the URL: %v", err)) 466 if values.Get(retryCountKey) != "" { 467 t.Fatalf("no retry counter should be attached: %v", retryCountKey) 468 } 469 } 470 471 func TestIsRetryable(t *testing.T) { 472 tcs := []struct { 473 req *http.Request 474 res *http.Response 475 err error 476 expected bool 477 }{ 478 { 479 req: nil, 480 res: nil, 481 err: nil, 482 expected: false, 483 }, 484 { 485 req: nil, 486 res: &http.Response{StatusCode: http.StatusBadRequest}, 487 err: nil, 488 expected: false, 489 }, 490 { 491 req: &http.Request{URL: &url.URL{Path: loginRequestPath}}, 492 res: nil, 493 err: nil, 494 expected: false, 495 }, 496 { 497 req: &http.Request{URL: &url.URL{Path: loginRequestPath}}, 498 res: &http.Response{StatusCode: http.StatusNotFound}, 499 expected: false, 500 }, 501 { 502 req: &http.Request{URL: &url.URL{Path: loginRequestPath}}, 503 res: nil, 504 err: errUnknownError(), 505 expected: true, 506 }, 507 { 508 req: &http.Request{URL: &url.URL{Path: loginRequestPath}}, 509 res: &http.Response{StatusCode: http.StatusTooManyRequests}, 510 err: nil, 511 expected: true, 512 }, 513 { 514 req: &http.Request{URL: &url.URL{Path: queryRequestPath}}, 515 res: &http.Response{StatusCode: http.StatusServiceUnavailable}, 516 err: nil, 517 expected: true, 518 }, 519 } 520 521 for _, tc := range tcs { 522 t.Run(fmt.Sprintf("req %v, resp %v", tc.req, tc.res), func(t *testing.T) { 523 result, _ := isRetryableError(tc.req, tc.res, tc.err) 524 if result != tc.expected { 525 t.Fatalf("expected %v, got %v; request: %v, response: %v", tc.expected, result, tc.req, tc.res) 526 } 527 }) 528 } 529 } 530 531 func TestCalculateRetryWait(t *testing.T) { 532 // test for randomly selected attempt and currWaitTime values 533 // minSleepTime, maxSleepTime are limit values 534 tcs := []struct { 535 attempt int 536 currWaitTime float64 537 minSleepTime float64 538 maxSleepTime float64 539 }{ 540 { 541 attempt: 1, 542 currWaitTime: 3.346609, 543 minSleepTime: 0.326695, 544 maxSleepTime: 5.019914, 545 }, 546 { 547 attempt: 2, 548 currWaitTime: 4.260357, 549 minSleepTime: 1.869821, 550 maxSleepTime: 6.390536, 551 }, 552 { 553 attempt: 3, 554 currWaitTime: 7.857728, 555 minSleepTime: 3.928864, 556 maxSleepTime: 11.928864, 557 }, 558 { 559 attempt: 4, 560 currWaitTime: 7.249255, 561 minSleepTime: 3.624628, 562 maxSleepTime: 19.624628, 563 }, 564 { 565 attempt: 5, 566 currWaitTime: 23.598257, 567 minSleepTime: 11.799129, 568 maxSleepTime: 43.799129, 569 }, 570 { 571 attempt: 8, 572 currWaitTime: 27.088613, 573 minSleepTime: 13.544306, 574 maxSleepTime: 269.544306, 575 }, 576 { 577 attempt: 10, 578 currWaitTime: 30.879329, 579 minSleepTime: 15.439664, 580 maxSleepTime: 1039.439664, 581 }, 582 { 583 attempt: 12, 584 currWaitTime: 39.919798, 585 minSleepTime: 19.959899, 586 maxSleepTime: 4115.959899, 587 }, 588 { 589 attempt: 15, 590 currWaitTime: 33.750758, 591 minSleepTime: 16.875379, 592 maxSleepTime: 32784.875379, 593 }, 594 { 595 attempt: 20, 596 currWaitTime: 32.357793, 597 minSleepTime: 16.178897, 598 maxSleepTime: 1048592.178897, 599 }, 600 } 601 602 for _, tc := range tcs { 603 t.Run(fmt.Sprintf("attmept: %v", tc.attempt), func(t *testing.T) { 604 result := defaultWaitAlgo.calculateWaitBeforeRetryForAuthRequest(tc.attempt, time.Duration(tc.currWaitTime*float64(time.Second))) 605 assertBetweenE(t, result.Seconds(), tc.minSleepTime, tc.maxSleepTime) 606 }) 607 } 608 } 609 610 func TestCalculateRetryWaitForNonAuthRequests(t *testing.T) { 611 // test for randomly selected currWaitTime values 612 // maxSleepTime is the limit value 613 tcs := []struct { 614 currWaitTime float64 615 maxSleepTime float64 616 }{ 617 { 618 currWaitTime: 3.346609, 619 maxSleepTime: 10.039827, 620 }, 621 { 622 currWaitTime: 4.260357, 623 maxSleepTime: 12.781071, 624 }, 625 { 626 currWaitTime: 5.154231, 627 maxSleepTime: 15.462693, 628 }, 629 { 630 currWaitTime: 7.249255, 631 maxSleepTime: 16, 632 }, 633 { 634 currWaitTime: 23.598257, 635 maxSleepTime: 16, 636 }, 637 } 638 639 for _, tc := range tcs { 640 defaultMinSleepTime := 1 641 t.Run(fmt.Sprintf("currWaitTime: %v", tc.currWaitTime), func(t *testing.T) { 642 result := defaultWaitAlgo.calculateWaitBeforeRetry(time.Duration(tc.currWaitTime) * time.Second) 643 assertBetweenInclusiveE(t, result.Seconds(), float64(defaultMinSleepTime), tc.maxSleepTime) 644 }) 645 } 646 }