Skip to content

Commit 337871b

Browse files
authored
chore: [Foss] Sync public: Add retry and logging for dlq/http/cors flaky requests (#2428)
* add retry and logging for dlq/http/cors flaky requests * add retry library to dev.txt * black reformat
1 parent 5f7c1ce commit 337871b

File tree

7 files changed

+121
-27
lines changed

7 files changed

+121
-27
lines changed

integration/combination/test_api_with_cors.py

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from integration.helpers.base_test import BaseTest
12
import requests
23
from unittest.case import skipIf
34

@@ -7,8 +8,6 @@
78
from integration.helpers.deployer.utils.retry import retry
89
from parameterized import parameterized
910

10-
from integration.helpers.exception import StatusCodeError
11-
1211
ALL_METHODS = "DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT"
1312

1413

@@ -30,8 +29,8 @@ def test_cors(self, file_name):
3029
allow_headers = "headers"
3130
max_age = "600"
3231

33-
self.verify_options_request(base_url + "/apione", allow_methods, allow_origin, allow_headers, max_age)
34-
self.verify_options_request(base_url + "/apitwo", allow_methods, allow_origin, allow_headers, max_age)
32+
self.verify_cors_options_request(base_url + "/apione", allow_methods, allow_origin, allow_headers, max_age)
33+
self.verify_cors_options_request(base_url + "/apitwo", allow_methods, allow_origin, allow_headers, max_age)
3534

3635
def test_cors_with_shorthand_notation(self):
3736
self.create_and_verify_stack("combination/api_with_cors_shorthand")
@@ -42,8 +41,8 @@ def test_cors_with_shorthand_notation(self):
4241
allow_headers = None # This should be absent from response
4342
max_age = None # This should be absent from response
4443

45-
self.verify_options_request(base_url + "/apione", ALL_METHODS, allow_origin, allow_headers, max_age)
46-
self.verify_options_request(base_url + "/apitwo", "OPTIONS,POST", allow_origin, allow_headers, max_age)
44+
self.verify_cors_options_request(base_url + "/apione", ALL_METHODS, allow_origin, allow_headers, max_age)
45+
self.verify_cors_options_request(base_url + "/apitwo", "OPTIONS,POST", allow_origin, allow_headers, max_age)
4746

4847
def test_cors_with_only_methods(self):
4948
self.create_and_verify_stack("combination/api_with_cors_only_methods")
@@ -55,8 +54,8 @@ def test_cors_with_only_methods(self):
5554
allow_headers = None # This should be absent from response
5655
max_age = None # This should be absent from response
5756

58-
self.verify_options_request(base_url + "/apione", allow_methods, allow_origin, allow_headers, max_age)
59-
self.verify_options_request(base_url + "/apitwo", allow_methods, allow_origin, allow_headers, max_age)
57+
self.verify_cors_options_request(base_url + "/apione", allow_methods, allow_origin, allow_headers, max_age)
58+
self.verify_cors_options_request(base_url + "/apitwo", allow_methods, allow_origin, allow_headers, max_age)
6059

6160
def test_cors_with_only_headers(self):
6261
self.create_and_verify_stack("combination/api_with_cors_only_headers")
@@ -67,8 +66,8 @@ def test_cors_with_only_headers(self):
6766
allow_headers = "headers"
6867
max_age = None # This should be absent from response
6968

70-
self.verify_options_request(base_url + "/apione", ALL_METHODS, allow_origin, allow_headers, max_age)
71-
self.verify_options_request(base_url + "/apitwo", "OPTIONS,POST", allow_origin, allow_headers, max_age)
69+
self.verify_cors_options_request(base_url + "/apione", ALL_METHODS, allow_origin, allow_headers, max_age)
70+
self.verify_cors_options_request(base_url + "/apitwo", "OPTIONS,POST", allow_origin, allow_headers, max_age)
7271

7372
def test_cors_with_only_max_age(self):
7473
self.create_and_verify_stack("combination/api_with_cors_only_max_age")
@@ -79,17 +78,12 @@ def test_cors_with_only_max_age(self):
7978
allow_headers = None
8079
max_age = "600"
8180

82-
self.verify_options_request(base_url + "/apione", ALL_METHODS, allow_origin, allow_headers, max_age)
83-
self.verify_options_request(base_url + "/apitwo", "OPTIONS,POST", allow_origin, allow_headers, max_age)
81+
self.verify_cors_options_request(base_url + "/apione", ALL_METHODS, allow_origin, allow_headers, max_age)
82+
self.verify_cors_options_request(base_url + "/apitwo", "OPTIONS,POST", allow_origin, allow_headers, max_age)
8483

85-
@retry(StatusCodeError, 3)
86-
def verify_options_request(self, url, allow_methods, allow_origin, allow_headers, max_age):
87-
response = requests.options(url)
88-
status = response.status_code
89-
if status != 200:
90-
raise StatusCodeError("Request to {} failed with status: {}, expected status: 200".format(url, status))
84+
def verify_cors_options_request(self, url, allow_methods, allow_origin, allow_headers, max_age):
85+
response = self.verify_options_request(url, 200)
9186

92-
self.assertEqual(status, 200, "Options request must be successful and return HTTP 200")
9387
headers = response.headers
9488
self.assertEqual(
9589
headers.get("Access-Control-Allow-Methods"), allow_methods, "Allow-Methods header must have proper value"

integration/combination/test_api_with_gateway_responses.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import logging
12
from unittest.case import skipIf
23

4+
from tenacity import stop_after_attempt, retry_if_exception_type, after_log, wait_exponential, retry, wait_random
5+
36
from integration.helpers.base_test import BaseTest
4-
from integration.helpers.deployer.utils.retry import retry
57
from integration.helpers.resource import current_region_does_not_support
68
from integration.config.service_names import GATEWAY_RESPONSES
79

10+
LOG = logging.getLogger(__name__)
11+
812

913
@skipIf(
1014
current_region_does_not_support([GATEWAY_RESPONSES]), "GatewayResponses is not supported in this testing region"
@@ -33,7 +37,13 @@ def test_gateway_responses(self):
3337
base_url = stack_outputs["ApiUrl"]
3438
self._verify_request_response_and_cors(base_url + "iam", 403)
3539

36-
@retry(AssertionError, exc_raise=AssertionError, exc_raise_msg="Unable to verify GatewayResponse request.")
40+
@retry(
41+
stop=stop_after_attempt(5),
42+
wait=wait_exponential(multiplier=1, min=4, max=10) + wait_random(0, 1),
43+
retry=retry_if_exception_type(AssertionError),
44+
after=after_log(LOG, logging.WARNING),
45+
reraise=True,
46+
)
3747
def _verify_request_response_and_cors(self, url, expected_response):
3848
response = self.verify_get_request_response(url, expected_response)
3949
access_control_allow_origin = response.headers.get("Access-Control-Allow-Origin", "")

integration/combination/test_function_with_cwe_dlq_generated.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from unittest.case import skipIf
33

44
from integration.helpers.base_test import BaseTest
5+
from integration.helpers.common_api import get_queue_policy
56
from integration.helpers.resource import first_item_in_dict, current_region_does_not_support
67
from integration.config.service_names import CWE_CWS_DLQ
78

@@ -30,9 +31,7 @@ def test_function_with_cwe(self):
3031

3132
# checking if the generated dead-letter queue has necessary resource based policy attached to it
3233
sqs_client = self.client_provider.sqs_client
33-
dlq_policy_result = sqs_client.get_queue_attributes(QueueUrl=lambda_target_dlq_url, AttributeNames=["Policy"])
34-
dlq_policy_doc = dlq_policy_result["Attributes"]["Policy"]
35-
dlq_policy = json.loads(dlq_policy_doc)["Statement"]
34+
dlq_policy = get_queue_policy(queue_url=lambda_target_dlq_url, sqs_client=sqs_client)
3635
self.assertEqual(len(dlq_policy), 1, "Only one statement must be in Dead-letter queue policy")
3736
dlq_policy_statement = dlq_policy[0]
3837

integration/helpers/base_test.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,22 @@
1010
from integration.helpers.client_provider import ClientProvider
1111
from integration.helpers.deployer.exceptions.exceptions import ThrottlingError
1212
from integration.helpers.deployer.utils.retry import retry_with_exponential_backoff_and_jitter
13+
from integration.helpers.exception import StatusCodeError
1314
from integration.helpers.request_utils import RequestUtils
1415
from integration.helpers.resource import generate_suffix, create_bucket, verify_stack_resources
1516
from integration.helpers.s3_uploader import S3Uploader
1617
from integration.helpers.yaml_utils import dump_yaml, load_yaml
1718
from samtranslator.yaml_helper import yaml_parse
1819

20+
from tenacity import (
21+
retry,
22+
stop_after_attempt,
23+
wait_exponential,
24+
retry_if_exception_type,
25+
after_log,
26+
wait_random,
27+
)
28+
1929
try:
2030
from pathlib import Path
2131
except ImportError:
@@ -502,6 +512,13 @@ def verify_stack(self, end_state="CREATE_COMPLETE"):
502512
if error:
503513
self.fail(error)
504514

515+
@retry(
516+
stop=stop_after_attempt(3),
517+
wait=wait_exponential(multiplier=1, min=4, max=10) + wait_random(0, 1),
518+
retry=retry_if_exception_type(StatusCodeError),
519+
after=after_log(LOG, logging.WARNING),
520+
reraise=True,
521+
)
505522
def verify_get_request_response(self, url, expected_status_code, headers=None):
506523
"""
507524
Verify if the get request to a certain url return the expected status code
@@ -510,13 +527,47 @@ def verify_get_request_response(self, url, expected_status_code, headers=None):
510527
----------
511528
url : string
512529
the url for the get request
513-
expected_status_code : string
530+
expected_status_code : int
514531
the expected status code
515532
headers : dict
516533
headers to use in request
517534
"""
518535
response = BaseTest.do_get_request_with_logging(url, headers)
519-
self.assertEqual(response.status_code, expected_status_code, " must return HTTP " + str(expected_status_code))
536+
if response.status_code != expected_status_code:
537+
raise StatusCodeError(
538+
"Request to {} failed with status: {}, expected status: {}".format(
539+
url, response.status_code, expected_status_code
540+
)
541+
)
542+
return response
543+
544+
@retry(
545+
stop=stop_after_attempt(3),
546+
wait=wait_exponential(multiplier=1, min=4, max=10) + wait_random(0, 1),
547+
retry=retry_if_exception_type(StatusCodeError),
548+
after=after_log(LOG, logging.WARNING),
549+
reraise=True,
550+
)
551+
def verify_options_request(self, url, expected_status_code, headers=None):
552+
"""
553+
Verify if the option request to a certain url return the expected status code
554+
555+
Parameters
556+
----------
557+
url : string
558+
the url for the get request
559+
expected_status_code : int
560+
the expected status code
561+
headers : dict
562+
headers to use in request
563+
"""
564+
response = BaseTest.do_options_request_with_logging(url, headers)
565+
if response.status_code != expected_status_code:
566+
raise StatusCodeError(
567+
"Request to {} failed with status: {}, expected status: {}".format(
568+
url, response.status_code, expected_status_code
569+
)
570+
)
520571
return response
521572

522573
def get_default_test_template_parameters(self):
@@ -570,3 +621,19 @@ def do_get_request_with_logging(url, headers=None):
570621
amazon_headers = RequestUtils(response).get_amazon_headers()
571622
REQUEST_LOGGER.info("Request made to " + url, extra={"status": response.status_code, "headers": amazon_headers})
572623
return response
624+
625+
@staticmethod
626+
def do_options_request_with_logging(url, headers=None):
627+
"""
628+
Perform a options request to an APIGW endpoint and log relevant info
629+
Parameters
630+
----------
631+
url : string
632+
the url for the get request
633+
headers : dict
634+
headers to use in request
635+
"""
636+
response = requests.options(url, headers=headers) if headers else requests.get(url)
637+
amazon_headers = RequestUtils(response).get_amazon_headers()
638+
REQUEST_LOGGER.info("Request made to " + url, extra={"status": response.status_code, "headers": amazon_headers})
639+
return response

integration/helpers/common_api.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
import json
2+
import logging
23

4+
from tenacity import (
5+
retry,
6+
stop_after_attempt,
7+
wait_exponential,
8+
retry_if_exception_type,
9+
after_log,
10+
wait_random,
11+
)
312

13+
LOG = logging.getLogger(__name__)
14+
15+
16+
@retry(
17+
stop=stop_after_attempt(3),
18+
wait=wait_exponential(multiplier=1, min=4, max=10) + wait_random(0, 1),
19+
retry=retry_if_exception_type(KeyError),
20+
after=after_log(LOG, logging.WARNING),
21+
reraise=True,
22+
)
423
def get_queue_policy(queue_url, sqs_client):
524
result = sqs_client.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["Policy"])
625
policy_document = result["Attributes"]["Policy"]

integration/single/test_basic_function.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def test_function_with_http_api_events(self, file_name):
4242

4343
endpoint = self.get_api_v2_endpoint("MyHttpApi")
4444

45-
self.assertEqual(BaseTest.do_get_request_with_logging(endpoint).text, self.FUNCTION_OUTPUT)
45+
self._verify_get_request(endpoint, self.FUNCTION_OUTPUT)
4646

4747
@parameterized.expand(
4848
[
@@ -283,3 +283,7 @@ def _assert_invoke(self, lambda_client, function_name, qualifier=None, expected_
283283

284284
response = lambda_client.invoke(**request_params)
285285
self.assertEqual(response.get("StatusCode"), expected_status_code)
286+
287+
def _verify_get_request(self, url, expected_text):
288+
response = self.verify_get_request_response(url, 200)
289+
self.assertEqual(response.text, expected_text)

requirements/dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ parameterized~=0.7.4
1515
click~=7.1
1616
dateparser~=0.7
1717
boto3>=1.23,<2
18+
tenacity~=7.0.0
1819

1920
# Requirements for examples
2021
requests~=2.24.0

0 commit comments

Comments
 (0)