Skip to content

Commit be6aa08

Browse files
author
Tom McCarthy
authored
fix: batch processing exceptions (#276)
* fix: Add tracebacks for all caught exceptions to SQSBatchProcessingError output * chore: Remove debugging code * chore: fix unit test to account for change in exception passing * chore: use kwargs in method calls, add comment to SQSBatchProcessingError * chore: Fix tests after changing method calls to use kwargs
1 parent 575a103 commit be6aa08

File tree

5 files changed

+45
-17
lines changed

5 files changed

+45
-17
lines changed

aws_lambda_powertools/utilities/batch/base.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def success_handler(self, record: Any, result: Any):
8585
self.success_messages.append(record)
8686
return entry
8787

88-
def failure_handler(self, record: Any, exception: Exception):
88+
def failure_handler(self, record: Any, exception: Tuple):
8989
"""
9090
Failure callback
9191
@@ -94,8 +94,9 @@ def failure_handler(self, record: Any, exception: Exception):
9494
tuple
9595
"fail", exceptions args, original record
9696
"""
97-
entry = ("fail", exception.args, record)
98-
logger.debug(f"Record processing exception: {exception}")
97+
exception_string = f"{exception[0]}:{exception[1]}"
98+
entry = ("fail", exception_string, record)
99+
logger.debug(f"Record processing exception: {exception_string}")
99100
self.exceptions.append(exception)
100101
self.fail_messages.append(record)
101102
return entry
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
"""
22
Batch processing exceptions
33
"""
4+
import traceback
45

56

67
class SQSBatchProcessingError(Exception):
78
"""When at least one message within a batch could not be processed"""
9+
10+
def __init__(self, msg="", child_exceptions=()):
11+
super().__init__(msg)
12+
self.msg = msg
13+
self.child_exceptions = child_exceptions
14+
15+
# Overriding this method so we can output all child exception tracebacks when we raise this exception to prevent
16+
# errors being lost. See https://github.com/awslabs/aws-lambda-powertools-python/issues/275
17+
def __str__(self):
18+
parent_exception_str = super(SQSBatchProcessingError, self).__str__()
19+
exception_list = [f"{parent_exception_str}\n"]
20+
for exception in self.child_exceptions:
21+
extype, ex, tb = exception
22+
formatted = "".join(traceback.format_exception(extype, ex, tb))
23+
exception_list.append(formatted)
24+
25+
return "\n".join(exception_list)

aws_lambda_powertools/utilities/batch/sqs.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Batch SQS utilities
55
"""
66
import logging
7+
import sys
78
from typing import Callable, Dict, List, Optional, Tuple
89

910
import boto3
@@ -90,10 +91,10 @@ def _process_record(self, record) -> Tuple:
9091
An object to be processed.
9192
"""
9293
try:
93-
result = self.handler(record)
94-
return self.success_handler(record, result)
95-
except Exception as exc:
96-
return self.failure_handler(record, exc)
94+
result = self.handler(record=record)
95+
return self.success_handler(record=record, result=result)
96+
except Exception:
97+
return self.failure_handler(record=record, exception=sys.exc_info())
9798

9899
def _prepare(self):
99100
"""
@@ -123,7 +124,11 @@ def _clean(self):
123124
logger.debug(f"{len(self.fail_messages)} records failed processing, but exceptions are suppressed")
124125
else:
125126
logger.debug(f"{len(self.fail_messages)} records failed processing, raising exception")
126-
raise SQSBatchProcessingError(list(self.exceptions))
127+
raise SQSBatchProcessingError(
128+
msg=f"Not all records processed succesfully. {len(self.exceptions)} individual errors logged "
129+
f"separately below.",
130+
child_exceptions=self.exceptions,
131+
)
127132

128133
return delete_message_response
129134

tests/functional/test_utilities_batch.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def test_partial_sqs_processor_context_with_failure(sqs_event_factory, record_ha
8585
with partial_processor(records, record_handler) as ctx:
8686
ctx.process()
8787

88-
assert len(error.value.args[0]) == 1
88+
assert len(error.value.child_exceptions) == 1
8989
stubber.assert_no_pending_responses()
9090

9191

@@ -144,7 +144,7 @@ def lambda_handler(event, context):
144144
with pytest.raises(SQSBatchProcessingError) as error:
145145
lambda_handler(event, {})
146146

147-
assert len(error.value.args[0]) == 2
147+
assert len(error.value.child_exceptions) == 2
148148
stubber.assert_no_pending_responses()
149149

150150

@@ -171,7 +171,7 @@ def lambda_handler(event, context):
171171
with pytest.raises(SQSBatchProcessingError) as error:
172172
lambda_handler(event, {})
173173

174-
assert len(error.value.args[0]) == 1
174+
assert len(error.value.child_exceptions) == 1
175175
stubber.assert_no_pending_responses()
176176

177177

@@ -203,7 +203,7 @@ def lambda_handler(event, context):
203203

204204
stubber.assert_no_pending_responses()
205205

206-
assert len(error.value.args[0]) == 1
206+
assert len(error.value.child_exceptions) == 1
207207
assert capsys.readouterr().out == "Oh no ! It's a failure.\n"
208208

209209

@@ -289,4 +289,4 @@ def test_partial_sqs_processor_context_only_failure(sqs_event_factory, record_ha
289289
with partial_processor(records, record_handler) as ctx:
290290
ctx.process()
291291

292-
assert len(error.value.args[0]) == 2
292+
assert len(error.value.child_exceptions) == 2

tests/unit/test_utilities_batch.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ def test_partial_sqs_process_record_success(mocker, partial_sqs_processor):
8181

8282
result = partial_sqs_processor._process_record(record)
8383

84-
handler_mock.assert_called_once_with(record)
85-
success_handler_mock.assert_called_once_with(record, success_result)
84+
handler_mock.assert_called_once_with(record=record)
85+
success_handler_mock.assert_called_once_with(record=record, result=success_result)
8686

8787
assert result == expected_value
8888

@@ -98,9 +98,13 @@ def test_partial_sqs_process_record_failure(mocker, partial_sqs_processor):
9898

9999
result = partial_sqs_processor._process_record(record)
100100

101-
handler_mock.assert_called_once_with(record)
102-
failure_handler_mock.assert_called_once_with(record, failure_result)
101+
handler_mock.assert_called_once_with(record=record)
103102

103+
_, failure_handler_called_with_args = failure_handler_mock.call_args
104+
failure_handler_mock.assert_called_once()
105+
assert (failure_handler_called_with_args["record"]) == record
106+
assert isinstance(failure_handler_called_with_args["exception"], tuple)
107+
assert failure_handler_called_with_args["exception"][1] == failure_result
104108
assert result == expected_value
105109

106110

0 commit comments

Comments
 (0)