Skip to content

Commit dd3707d

Browse files
author
Dan Straw
committed
feat: add ReturnValuesOnConditionCheckFailure to DynamoDB idempotency to return a copy of the item on failure and avoid a subsequent get aws-powertools#3327. Changes after PR comments
1 parent 5727c48 commit dd3707d

File tree

3 files changed

+39
-48
lines changed

3 files changed

+39
-48
lines changed

aws_lambda_powertools/utilities/idempotency/exceptions.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,16 @@ class IdempotencyItemAlreadyExistsError(BaseError):
3333
"""
3434

3535
def __init__(self, *args: Optional[Union[str, Exception]], old_data_record: Optional[DataRecord] = None):
36-
self.message = str(args[0]) if args else ""
37-
self.details = "".join(str(arg) for arg in args[1:]) if args[1:] else None
3836
self.old_data_record = old_data_record
37+
super().__init__(*args)
3938

4039
def __str__(self):
4140
"""
4241
Return all arguments formatted or original message
4342
"""
4443
old_data_record = f" from [{(str(self.old_data_record))}]" if self.old_data_record else ""
45-
details = f" - ({self.details})" if self.details else ""
46-
47-
return f"{self.message}{details}{old_data_record}"
44+
message = super().__str__()
45+
return f"{message}{old_data_record}"
4846

4947

5048
class IdempotencyItemNotFoundError(BaseError):

aws_lambda_powertools/utilities/idempotency/persistence/base.py

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import os
99
import warnings
1010
from abc import ABC, abstractmethod
11-
from typing import Any, Dict, Optional
11+
from typing import Any, Dict, Optional, Union
1212

1313
import jmespath
1414

@@ -160,16 +160,20 @@ def _generate_hash(self, data: Any) -> str:
160160
hashed_data = self.hash_function(json.dumps(data, cls=Encoder, sort_keys=True).encode())
161161
return hashed_data.hexdigest()
162162

163-
def _validate_payload(self, data: Dict[str, Any], data_record: DataRecord) -> None:
163+
def _validate_payload(
164+
self,
165+
data_payload: Union[Dict[str, Any], DataRecord],
166+
stored_data_record: DataRecord,
167+
) -> None:
164168
"""
165169
Validate that the hashed payload matches data provided and stored data record
166170
167171
Parameters
168172
----------
169-
data: Dict[str, Any]
173+
data_payload: Union[Dict[str, Any], DataRecord]
170174
Payload
171-
data_record: DataRecord
172-
DataRecord instance
175+
stored_data_record: DataRecord
176+
DataRecord fetched from Dynamo or cache
173177
174178
Raises
175179
----------
@@ -178,30 +182,13 @@ def _validate_payload(self, data: Dict[str, Any], data_record: DataRecord) -> No
178182
179183
"""
180184
if self.payload_validation_enabled:
181-
data_hash = self._get_hashed_payload(data=data)
182-
if data_record.payload_hash != data_hash:
183-
raise IdempotencyValidationError("Payload does not match stored record for this event key")
184-
185-
def _validate_hashed_payload(self, old_data_record: DataRecord, data_record: DataRecord) -> None:
186-
"""
187-
Validate that the hashed data provided matches the payload_hash stored data record
188-
189-
Parameters
190-
----------
191-
old_data_record: DataRecord
192-
DataRecord instance fetched from Dynamo
193-
data_record: DataRecord
194-
DataRecord instance which failed insert into Dynamo
185+
if isinstance(data_payload, DataRecord):
186+
data_hash = data_payload.payload_hash
187+
else:
188+
data_hash = self._get_hashed_payload(data=data_payload)
195189

196-
Raises
197-
----------
198-
IdempotencyValidationError
199-
Payload doesn't match the stored record for the given idempotency key
200-
201-
"""
202-
if self.payload_validation_enabled:
203-
if old_data_record.payload_hash != data_record.payload_hash:
204-
raise IdempotencyValidationError("Hashed payload does not match stored record for this event key")
190+
if stored_data_record.payload_hash != data_hash:
191+
raise IdempotencyValidationError("Payload does not match stored record for this event key")
205192

206193
def _get_expiry_timestamp(self) -> int:
207194
"""
@@ -391,14 +378,14 @@ def get_record(self, data: Dict[str, Any]) -> Optional[DataRecord]:
391378
cached_record = self._retrieve_from_cache(idempotency_key=idempotency_key)
392379
if cached_record:
393380
logger.debug(f"Idempotency record found in cache with idempotency key: {idempotency_key}")
394-
self._validate_payload(data=data, data_record=cached_record)
381+
self._validate_payload(data_payload=data, stored_data_record=cached_record)
395382
return cached_record
396383

397384
record = self._get_record(idempotency_key=idempotency_key)
398385

399386
self._save_to_cache(data_record=record)
400387

401-
self._validate_payload(data=data, data_record=record)
388+
self._validate_payload(data_payload=data, stored_data_record=record)
402389
return record
403390

404391
@abstractmethod

aws_lambda_powertools/utilities/idempotency/persistence/dynamodb.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,9 @@ def _put_record(self, data_record: DataRecord) -> None:
257257
self._save_to_cache(data_record=old_data_record)
258258

259259
try:
260-
self._validate_hashed_payload(old_data_record=old_data_record, data_record=data_record)
261-
except IdempotencyValidationError as ive:
262-
raise ive from exc
260+
self._validate_payload(data_payload=data_record, stored_data_record=old_data_record)
261+
except IdempotencyValidationError as idempotency_validation_error:
262+
raise idempotency_validation_error from exc
263263

264264
raise IdempotencyItemAlreadyExistsError(old_data_record=old_data_record) from exc
265265

@@ -271,17 +271,23 @@ def _put_record(self, data_record: DataRecord) -> None:
271271
raise
272272

273273
@staticmethod
274-
def boto3_supports_condition_check_failure(boto3_version: str):
275-
version = boto3_version.split(".")
274+
def boto3_supports_condition_check_failure(boto3_version: str) -> bool:
275+
"""
276+
Check if the installed boto3 version supports condition check failure.
277+
278+
Params
279+
------
280+
boto3_version: str
281+
The boto3 version
282+
283+
Returns
284+
-------
285+
bool
286+
True if the boto3 version supports condition check failure, False otherwise.
287+
"""
276288
# Only supported in boto3 1.26.164 and above
277-
if len(version) >= 3 and int(version[0]) == 1 and int(version[1]) == 26 and int(version[2]) >= 164:
278-
return True
279-
if len(version) >= 2 and int(version[0]) == 1 and int(version[1]) > 26:
280-
return True
281-
if int(version[0]) > 1:
282-
return True
283-
284-
return False
289+
major, minor, *patch = map(int, boto3_version.split("."))
290+
return (major, minor, *patch) >= (1, 26, 164)
285291

286292
def _update_record(self, data_record: DataRecord):
287293
logger.debug(f"Updating record for idempotency key: {data_record.idempotency_key}")

0 commit comments

Comments
 (0)