Skip to content

Commit 292e9f4

Browse files
committed
Simplify SAR polling loop and add test coverage
1 parent 54a7c51 commit 292e9f4

File tree

2 files changed

+92
-78
lines changed

2 files changed

+92
-78
lines changed

samtranslator/plugins/application/serverless_app_plugin.py

Lines changed: 60 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,6 @@ 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
186183

187184
def _handle_create_cfn_template_request(self, app_id, semver, key, logical_id):
188185
"""
@@ -197,11 +194,7 @@ def _handle_create_cfn_template_request(self, app_id, semver, key, logical_id):
197194
create_cfn_template = lambda app_id, semver: self._sar_client.create_cloud_formation_template(
198195
ApplicationId=self._sanitize_sar_str_param(app_id), SemanticVersion=self._sanitize_sar_str_param(semver)
199196
)
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
197+
response = self._sar_service_call(create_cfn_template, logical_id, app_id, semver)
205198

206199
LOG.info("Requested to create CFN template {}/{} in serverless application repo.".format(app_id, semver))
207200
self._applications[key] = response[self.TEMPLATE_URL_KEY]
@@ -311,78 +304,73 @@ def on_after_transform_template(self, template):
311304
:param dict template: Dictionary of the SAM template
312305
:return: Nothing
313306
"""
314-
if self._wait_for_template_active_status and not self._validate_only:
315-
start_time = time()
316-
while (time() - start_time) < self.TEMPLATE_WAIT_TIMEOUT_SECONDS:
317-
temp = self._in_progress_templates
318-
self._in_progress_templates = []
319-
# it's either our first time in this loop or we just slept SLEEP_TIME_SECONDS so we can
320-
# try calling SAR
321-
throttled = False
322-
323-
# Check each resource to make sure it's active
324-
LOG.info("Checking resources in serverless application repo...")
325-
for application_id, template_id in temp:
326-
get_cfn_template = (
327-
lambda application_id, template_id: self._sar_client.get_cloud_formation_template(
328-
ApplicationId=self._sanitize_sar_str_param(application_id),
329-
TemplateId=self._sanitize_sar_str_param(template_id),
330-
)
331-
)
332-
response = {"Status": "PREPARING"} # default response if we can't reach SAR
333-
334-
try:
335-
if not throttled:
336-
response = self._sar_service_call(
337-
get_cfn_template, application_id, application_id, template_id
338-
)
339-
except ClientError as e:
340-
error_code = e.response["Error"]["Code"]
341-
if error_code == "TooManyRequestsException":
342-
# We were throttled by SAR, don't hammer SAR with more calls in this for loop
343-
throttled = True
344-
LOG.debug("SAR call timed out for application id {}".format(application_id))
345-
# don't re-raise, fall through to regular processing as if the template wasn't ready yet
346-
else:
347-
LOG.exception(e)
348-
raise e
349-
350-
self._handle_get_cfn_template_response(response, application_id, template_id)
351-
352-
LOG.info("Finished checking resources in serverless application repo.")
353-
354-
# Don't sleep if there are no more templates with PREPARING status
355-
if len(self._in_progress_templates) == 0:
356-
break
357-
358-
# Sleep a little so we don't spam service calls
359-
sleep(self.SLEEP_TIME_SECONDS)
360-
361-
# Not all templates reached active status
362-
if len(self._in_progress_templates) != 0:
363-
application_ids = [items[0] for items in self._in_progress_templates]
364-
raise InvalidResourceException(
365-
application_ids, "Timed out waiting for nested stack templates " "to reach ACTIVE status."
307+
if not self._wait_for_template_active_status or self._validate_only:
308+
return
309+
310+
start_time = time()
311+
while (time() - start_time) < self.TEMPLATE_WAIT_TIMEOUT_SECONDS:
312+
# Check each resource to make sure it's active
313+
LOG.info("Checking resources in serverless application repo...")
314+
idx = 0
315+
while idx < len(self._in_progress_templates):
316+
application_id, template_id = self._in_progress_templates[idx]
317+
get_cfn_template = lambda application_id, template_id: self._sar_client.get_cloud_formation_template(
318+
ApplicationId=self._sanitize_sar_str_param(application_id),
319+
TemplateId=self._sanitize_sar_str_param(template_id),
366320
)
367321

368-
def _handle_get_cfn_template_response(self, response, application_id, template_id):
322+
try:
323+
response = self._sar_service_call(get_cfn_template, application_id, application_id, template_id)
324+
except ClientError as e:
325+
error_code = e.response["Error"]["Code"]
326+
if error_code == "TooManyRequestsException":
327+
LOG.debug("SAR call timed out for application id {}".format(application_id))
328+
break # We were throttled by SAR, break out to a sleep
329+
else:
330+
raise e
331+
332+
if self._is_template_active(response, application_id, template_id):
333+
self._in_progress_templates.remove((application_id, template_id))
334+
else:
335+
idx += 1 # check next template
336+
337+
LOG.info("Finished checking resources in serverless application repo.")
338+
339+
# Don't sleep if there are no more templates with PREPARING status
340+
if len(self._in_progress_templates) == 0:
341+
break
342+
343+
# Sleep a little so we don't spam service calls
344+
sleep(self._get_sleep_time_sec())
345+
346+
# Not all templates reached active status
347+
if len(self._in_progress_templates) != 0:
348+
application_ids = [items[0] for items in self._in_progress_templates]
349+
raise InvalidResourceException(
350+
application_ids, "Timed out waiting for nested stack templates " "to reach ACTIVE status."
351+
)
352+
353+
def _get_sleep_time_sec(self):
354+
return self.SLEEP_TIME_SECONDS
355+
356+
def _is_template_active(self, response, application_id, template_id):
369357
"""
370-
Handles the response from the SAR service call
358+
Checks the response from a SAR service call; returns True if the template is active,
359+
throws an exception if the request expired and returns False in all other cases.
371360
372361
:param dict response: the response dictionary from the app repo
373362
:param string application_id: the ApplicationId
374363
:param string template_id: the unique TemplateId for this application
375364
"""
376-
status = response["Status"]
377-
if status != "ACTIVE":
378-
# Other options are PREPARING and EXPIRED.
379-
if status == "EXPIRED":
380-
message = (
381-
"Template for {} with id {} returned status: {}. Cannot access an expired "
382-
"template.".format(application_id, template_id, status)
383-
)
384-
raise InvalidResourceException(application_id, message)
385-
self._in_progress_templates.append((application_id, template_id))
365+
status = response["Status"] # options: PREPARING, EXPIRED or ACTIVE
366+
367+
if status == "EXPIRED":
368+
message = "Template for {} with id {} returned status: {}. Cannot access an expired " "template.".format(
369+
application_id, template_id, status
370+
)
371+
raise InvalidResourceException(application_id, message)
372+
373+
return status == "ACTIVE"
386374

387375
@cw_timer(prefix="External", name="SAR")
388376
def _sar_service_call(self, service_call_lambda, logical_id, *args):

tests/plugins/application/test_serverless_app_plugin.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,15 @@ def test_sar_throttling_doesnt_stop_processing(self):
264264
{"Error": {"Code": "TooManyRequestsException"}}, "GetCloudFormationTemplate"
265265
)
266266
plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False)
267-
plugin._in_progress_templates = [("appid", "template")]
268-
plugin.SLEEP_TIME_SECONDS = 0.05
269-
plugin.TEMPLATE_WAIT_TIMEOUT_SECONDS = 0.3
267+
plugin._get_sleep_time_sec = Mock()
268+
plugin._get_sleep_time_sec.return_value = 0.02
269+
plugin._in_progress_templates = [("appid", "template"), ("appid2", "template2")]
270+
plugin.TEMPLATE_WAIT_TIMEOUT_SECONDS = 0.2
270271
with self.assertRaises(InvalidResourceException):
271272
plugin.on_after_transform_template("template")
272-
# confirm we had at least two attempts to call SAR
273+
# confirm we had at least two attempts to call SAR and that we executed a sleep
273274
self.assertGreater(client.get_cloud_formation_template.call_count, 1)
275+
self.assertGreaterEqual(plugin._get_sleep_time_sec.call_count, 1)
274276

275277
def test_unexpected_sar_error_stops_processing(self):
276278
client = Mock()
@@ -286,7 +288,7 @@ def test_unexpected_sar_error_stops_processing(self):
286288
def test_sar_success_one_app(self):
287289
client = Mock()
288290
client.get_cloud_formation_template = Mock()
289-
client.get_cloud_formation_template.return_value = {"Status": "ACTIVE"}
291+
client.get_cloud_formation_template.return_value = {"Status": STATUS_ACTIVE}
290292
plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False)
291293
plugin._in_progress_templates = [("appid", "template")]
292294
plugin.on_after_transform_template("template")
@@ -296,9 +298,33 @@ def test_sar_success_one_app(self):
296298
def test_sar_success_two_apps(self):
297299
client = Mock()
298300
client.get_cloud_formation_template = Mock()
299-
client.get_cloud_formation_template.return_value = {"Status": "ACTIVE"}
301+
client.get_cloud_formation_template.return_value = {"Status": STATUS_ACTIVE}
300302
plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False)
301303
plugin._in_progress_templates = [("appid1", "template1"), ("appid2", "template2")]
302304
plugin.on_after_transform_template("template")
303305
# should have exactly one call to SAR per app
304306
self.assertEqual(client.get_cloud_formation_template.call_count, 2)
307+
308+
def test_expired_sar_app_throws(self):
309+
client = Mock()
310+
client.get_cloud_formation_template = Mock()
311+
client.get_cloud_formation_template.return_value = {"Status": STATUS_EXPIRED}
312+
plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False)
313+
plugin._in_progress_templates = [("appid1", "template1"), ("appid2", "template2")]
314+
with self.assertRaises(InvalidResourceException):
315+
plugin.on_after_transform_template("template")
316+
# should have exactly one call to SAR since the first app will be expired
317+
self.assertEqual(client.get_cloud_formation_template.call_count, 1)
318+
319+
def test_sleep_between_sar_checks(self):
320+
client = Mock()
321+
client.get_cloud_formation_template = Mock()
322+
client.get_cloud_formation_template.side_effect = [{"Status": STATUS_PREPARING}, {"Status": STATUS_ACTIVE}]
323+
plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False)
324+
plugin._in_progress_templates = [("appid1", "template1")]
325+
plugin._get_sleep_time_sec = Mock()
326+
plugin._get_sleep_time_sec.return_value = 0.001
327+
plugin.on_after_transform_template("template")
328+
# should have exactly two calls to SAR
329+
self.assertEqual(client.get_cloud_formation_template.call_count, 2)
330+
self.assertEqual(plugin._get_sleep_time_sec.call_count, 1) # make sure we slept once

0 commit comments

Comments
 (0)