Skip to content

Commit f2a2af2

Browse files
committed
Do not abort SAR loop on throttling
1 parent e816e50 commit f2a2af2

File tree

2 files changed

+80
-10
lines changed

2 files changed

+80
-10
lines changed

samtranslator/plugins/application/serverless_app_plugin.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ def _handle_get_application_request(self, app_id, semver, key, logical_id):
180180
warning_message = "{}. Unable to verify access to {}/{}.".format(e, app_id, semver)
181181
LOG.warning(warning_message)
182182
self._applications[key] = {"Unable to verify"}
183+
except ClientError as e:
184+
LOG.exception(e)
185+
raise e
183186

184187
def _handle_create_cfn_template_request(self, app_id, semver, key, logical_id):
185188
"""
@@ -194,7 +197,12 @@ def _handle_create_cfn_template_request(self, app_id, semver, key, logical_id):
194197
create_cfn_template = lambda app_id, semver: self._sar_client.create_cloud_formation_template(
195198
ApplicationId=self._sanitize_sar_str_param(app_id), SemanticVersion=self._sanitize_sar_str_param(semver)
196199
)
197-
response = self._sar_service_call(create_cfn_template, logical_id, app_id, semver)
200+
try:
201+
response = self._sar_service_call(create_cfn_template, logical_id, app_id, semver)
202+
except ClientError as e:
203+
LOG.exception(e)
204+
raise e
205+
198206
LOG.info("Requested to create CFN template {}/{} in serverless application repo.".format(app_id, semver))
199207
self._applications[key] = response[self.TEMPLATE_URL_KEY]
200208
if response["Status"] != "ACTIVE":
@@ -308,6 +316,7 @@ def on_after_transform_template(self, template):
308316
while (time() - start_time) < self.TEMPLATE_WAIT_TIMEOUT_SECONDS:
309317
temp = self._in_progress_templates
310318
self._in_progress_templates = []
319+
throttled = False
311320

312321
# Check each resource to make sure it's active
313322
LOG.info("Checking resources in serverless application repo...")
@@ -318,8 +327,26 @@ def on_after_transform_template(self, template):
318327
TemplateId=self._sanitize_sar_str_param(template_id),
319328
)
320329
)
321-
response = self._sar_service_call(get_cfn_template, application_id, application_id, template_id)
330+
response = {"Status": "PREPARING"} # default response if we can't reach SAR
331+
332+
try:
333+
if not throttled:
334+
response = self._sar_service_call(
335+
get_cfn_template, application_id, application_id, template_id
336+
)
337+
except ClientError as e:
338+
error_code = e.response["Error"]["Code"]
339+
if error_code == "TooManyRequestsException":
340+
# We were throttled by SAR, don't hammer SAR with more calls in this for loop
341+
throttled = True
342+
LOG.debug("SAR call timed out for application id {}".format(application_id))
343+
# don't re-raise, fall through to regular processing as if the template wasn't ready yet
344+
else:
345+
LOG.exception(e)
346+
raise e
347+
322348
self._handle_get_cfn_template_response(response, application_id, template_id)
349+
323350
LOG.info("Finished checking resources in serverless application repo.")
324351

325352
# Don't sleep if there are no more templates with PREPARING status
@@ -372,9 +399,6 @@ def _sar_service_call(self, service_call_lambda, logical_id, *args):
372399
error_code = e.response["Error"]["Code"]
373400
if error_code in ("AccessDeniedException", "NotFoundException"):
374401
raise InvalidResourceException(logical_id, e.response["Error"]["Message"])
375-
376-
# 'ForbiddenException'- SAR rejects connection
377-
LOG.exception(e)
378402
raise e
379403

380404
def _resource_is_supported(self, resource_type):

tests/plugins/application/test_serverless_app_plugin.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import boto3
22
import itertools
3+
from botocore.exceptions import ClientError
34

45
from mock import Mock, patch
56
from unittest import TestCase
67
from parameterized import parameterized, param
78

89
from samtranslator.plugins.application.serverless_app_plugin import ServerlessAppPlugin
910
from samtranslator.plugins.exceptions import InvalidPluginException
11+
from samtranslator.model.exceptions import InvalidResourceException
1012

1113
# TODO: run tests when AWS CLI is not configured (so they can run in brazil)
1214

@@ -253,9 +255,53 @@ def __init__(self, app_id="app_id", semver="1.3.5"):
253255

254256
# self.plugin.on_before_transform_resource(app_resources[0][0], 'AWS::Serverless::Application', app_resources[0][1].properties)
255257

256-
# class TestServerlessAppPlugin_on_after_transform_template(TestCase):
257258

258-
# def setUp(self):
259-
# self.plugin = SeverlessAppPlugin()
260-
261-
# # TODO: test this lifecycle event
259+
class TestServerlessAppPlugin_on_after_transform_template(TestCase):
260+
def setUp(self):
261+
pass
262+
263+
def test_sar_throttling_doesnt_stop_processing(self):
264+
client = Mock()
265+
client.get_cloud_formation_template = Mock()
266+
client.get_cloud_formation_template.side_effect = ClientError(
267+
{"Error": {"Code": "TooManyRequestsException"}}, "GetCloudFormationTemplate"
268+
)
269+
plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False)
270+
plugin._in_progress_templates = [("appid", "template")]
271+
plugin.SLEEP_TIME_SECONDS = 0.05
272+
plugin.TEMPLATE_WAIT_TIMEOUT_SECONDS = 0.3
273+
with self.assertRaises(InvalidResourceException):
274+
plugin.on_after_transform_template("template")
275+
# confirm we had at least two attempts to call SAR
276+
self.assertGreater(client.get_cloud_formation_template.call_count, 1)
277+
278+
def test_unexpected_sar_error_stops_processing(self):
279+
client = Mock()
280+
client.get_cloud_formation_template = Mock()
281+
client.get_cloud_formation_template.side_effect = ClientError(
282+
{"Error": {"Code": "BadBadError"}}, "GetCloudFormationTemplate"
283+
)
284+
plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False)
285+
plugin._in_progress_templates = [("appid", "template")]
286+
with self.assertRaises(ClientError):
287+
plugin.on_after_transform_template("template")
288+
289+
def test_sar_success_one_app(self):
290+
client = Mock()
291+
client.get_cloud_formation_template = Mock()
292+
client.get_cloud_formation_template.return_value = {"Status": "ACTIVE"}
293+
plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False)
294+
plugin._in_progress_templates = [("appid", "template")]
295+
plugin.on_after_transform_template("template")
296+
# should have exactly one call to SAR
297+
self.assertEqual(client.get_cloud_formation_template.call_count, 1)
298+
299+
def test_sar_success_two_apps(self):
300+
client = Mock()
301+
client.get_cloud_formation_template = Mock()
302+
client.get_cloud_formation_template.return_value = {"Status": "ACTIVE"}
303+
plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False)
304+
plugin._in_progress_templates = [("appid1", "template1"), ("appid2", "template2")]
305+
plugin.on_after_transform_template("template")
306+
# should have exactly one call to SAR per app
307+
self.assertEqual(client.get_cloud_formation_template.call_count, 2)

0 commit comments

Comments
 (0)