Skip to content

Commit 33fb5a4

Browse files
committed
chore(idempotency): add tests for expires_in_progress
1 parent 92cdcd1 commit 33fb5a4

File tree

5 files changed

+160
-8
lines changed

5 files changed

+160
-8
lines changed

aws_lambda_powertools/utilities/idempotency/persistence/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ def save_inprogress(self, data: Dict[str, Any], remaining_time_in_millis: Option
365365

366366
data_record.in_progress_expiry_timestamp = timestamp
367367
else:
368-
logger.debug("Expires in progress is enabled but we couldn't determine the remaining time left")
368+
warnings.warn("Expires in progress is enabled but we couldn't determine the remaining time left")
369369

370370
logger.debug(f"Saving in progress record for idempotency key: {data_record.idempotency_key}")
371371

aws_lambda_powertools/utilities/idempotency/persistence/dynamodb.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def _put_record(self, data_record: DataRecord) -> None:
180180
"#in_progress_expiry": self.in_progress_expiry_attr,
181181
"#status": self.status_attr,
182182
},
183-
ExpressionAttributeValues={":now": int(now.timestamp()), ":status": STATUS_CONSTANTS["INPROGRESS"]},
183+
ExpressionAttributeValues={":now": int(now.timestamp()), ":inprogress": STATUS_CONSTANTS["INPROGRESS"]},
184184
)
185185
except self.table.meta.client.exceptions.ConditionalCheckFailedException:
186186
logger.debug(f"Failed to put record for already existing idempotency key: {data_record.idempotency_key}")

tests/functional/idempotency/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def expected_params_put_item(hashed_idempotency_key):
136136
"#in_progress_expiry": "in_progress_expiration",
137137
"#status": "status",
138138
},
139-
"ExpressionAttributeValues": {":now": stub.ANY, ":status": "INPROGRESS"},
139+
"ExpressionAttributeValues": {":now": stub.ANY, ":inprogress": "INPROGRESS"},
140140
"Item": {"expiration": stub.ANY, "id": hashed_idempotency_key, "status": "INPROGRESS"},
141141
"TableName": "TEST_TABLE",
142142
}
@@ -156,7 +156,7 @@ def expected_params_put_item_with_validation(hashed_idempotency_key, hashed_vali
156156
"#now": stub.ANY,
157157
"#status": "status",
158158
},
159-
"ExpressionAttributeValues": {":now": stub.ANY, ":status": "INPROGRESS"},
159+
"ExpressionAttributeValues": {":now": stub.ANY, ":inprogress": "INPROGRESS"},
160160
"Item": {
161161
"expiration": stub.ANY,
162162
"id": hashed_idempotency_key,

tests/functional/idempotency/test_idempotency.py

Lines changed: 155 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import copy
2+
import datetime
23
import sys
4+
import warnings
35
from hashlib import md5
46
from unittest.mock import MagicMock
57

@@ -10,7 +12,7 @@
1012

1113
from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEventV2, event_source
1214
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig
13-
from aws_lambda_powertools.utilities.idempotency.base import _prepare_data
15+
from aws_lambda_powertools.utilities.idempotency.base import MAX_RETRIES, _prepare_data
1416
from aws_lambda_powertools.utilities.idempotency.exceptions import (
1517
IdempotencyAlreadyInProgressError,
1618
IdempotencyInconsistentStateError,
@@ -386,7 +388,7 @@ def test_idempotent_lambda_expired(
386388
lambda_context,
387389
):
388390
"""
389-
Test idempotent decorator when lambda is called with an event it succesfully handled already, but outside of the
391+
Test idempotent decorator when lambda is called with an event it successfully handled already, but outside of the
390392
expiry window
391393
"""
392394

@@ -529,7 +531,7 @@ def test_idempotent_lambda_expired_during_request(
529531
lambda_context,
530532
):
531533
"""
532-
Test idempotent decorator when lambda is called with an event it succesfully handled already. Persistence store
534+
Test idempotent decorator when lambda is called with an event it successfully handled already. Persistence store
533535
returns inconsistent/rapidly changing result between put_item and get_item calls.
534536
"""
535537

@@ -804,6 +806,156 @@ def lambda_handler(event, context):
804806
stubber.deactivate()
805807

806808

809+
@pytest.mark.parametrize(
810+
"idempotency_config",
811+
[
812+
{"use_local_cache": False, "expires_in_progress": True},
813+
{"use_local_cache": True, "expires_in_progress": True},
814+
],
815+
indirect=True,
816+
)
817+
def test_idempotent_lambda_expires_in_progress_before_expire(
818+
idempotency_config: IdempotencyConfig,
819+
persistence_store: DynamoDBPersistenceLayer,
820+
lambda_apigw_event,
821+
timestamp_future,
822+
lambda_response,
823+
hashed_idempotency_key,
824+
lambda_context,
825+
):
826+
"""
827+
Test idempotent decorator when expires_in_progress is on and the event is still in progress, before the
828+
lambda expiration window.
829+
"""
830+
831+
stubber = stub.Stubber(persistence_store.table.meta.client)
832+
833+
stubber.add_client_error("put_item", "ConditionalCheckFailedException")
834+
835+
now = datetime.datetime.now()
836+
period = datetime.timedelta(seconds=5)
837+
timestamp_expires_in_progress = str(int((now + period).timestamp()))
838+
839+
expected_params_get_item = {
840+
"TableName": TABLE_NAME,
841+
"Key": {"id": hashed_idempotency_key},
842+
"ConsistentRead": True,
843+
}
844+
ddb_response_get_item = {
845+
"Item": {
846+
"id": {"S": hashed_idempotency_key},
847+
"expiration": {"N": timestamp_future},
848+
"in_progress_expiration": {"N": timestamp_expires_in_progress},
849+
"data": {"S": '{"message": "test", "statusCode": 200'},
850+
"status": {"S": "INPROGRESS"},
851+
}
852+
}
853+
stubber.add_response("get_item", ddb_response_get_item, expected_params_get_item)
854+
855+
stubber.activate()
856+
857+
@idempotent(config=idempotency_config, persistence_store=persistence_store)
858+
def lambda_handler(event, context):
859+
return lambda_response
860+
861+
with pytest.raises(IdempotencyAlreadyInProgressError):
862+
lambda_handler(lambda_apigw_event, lambda_context)
863+
864+
stubber.assert_no_pending_responses()
865+
stubber.deactivate()
866+
867+
868+
@pytest.mark.parametrize(
869+
"idempotency_config",
870+
[
871+
{"use_local_cache": False, "expires_in_progress": True},
872+
{"use_local_cache": True, "expires_in_progress": True},
873+
],
874+
indirect=True,
875+
)
876+
def test_idempotent_lambda_expires_in_progress_after_expire(
877+
idempotency_config: IdempotencyConfig,
878+
persistence_store: DynamoDBPersistenceLayer,
879+
lambda_apigw_event,
880+
timestamp_future,
881+
lambda_response,
882+
hashed_idempotency_key,
883+
lambda_context,
884+
):
885+
stubber = stub.Stubber(persistence_store.table.meta.client)
886+
887+
for _ in range(MAX_RETRIES + 1):
888+
stubber.add_client_error("put_item", "ConditionalCheckFailedException")
889+
890+
one_second_ago = datetime.datetime.now() - datetime.timedelta(seconds=1)
891+
expected_params_get_item = {
892+
"TableName": TABLE_NAME,
893+
"Key": {"id": hashed_idempotency_key},
894+
"ConsistentRead": True,
895+
}
896+
ddb_response_get_item = {
897+
"Item": {
898+
"id": {"S": hashed_idempotency_key},
899+
"expiration": {"N": timestamp_future},
900+
"in_progress_expiration": {"N": str(int(one_second_ago.timestamp()))},
901+
"data": {"S": '{"message": "test", "statusCode": 200'},
902+
"status": {"S": "INPROGRESS"},
903+
}
904+
}
905+
stubber.add_response("get_item", ddb_response_get_item, expected_params_get_item)
906+
907+
stubber.activate()
908+
909+
@idempotent(config=idempotency_config, persistence_store=persistence_store)
910+
def lambda_handler(event, context):
911+
return lambda_response
912+
913+
with pytest.raises(IdempotencyInconsistentStateError):
914+
lambda_handler(lambda_apigw_event, lambda_context)
915+
916+
stubber.assert_no_pending_responses()
917+
stubber.deactivate()
918+
919+
920+
@pytest.mark.parametrize(
921+
"idempotency_config",
922+
[
923+
{"use_local_cache": False, "expires_in_progress": True},
924+
{"use_local_cache": True, "expires_in_progress": True},
925+
],
926+
indirect=True,
927+
)
928+
def test_idempotent_lambda_expires_in_progress_unavailable_remaining_time(
929+
idempotency_config: IdempotencyConfig,
930+
persistence_store: DynamoDBPersistenceLayer,
931+
lambda_apigw_event,
932+
lambda_response,
933+
lambda_context,
934+
expected_params_put_item,
935+
expected_params_update_item,
936+
):
937+
stubber = stub.Stubber(persistence_store.table.meta.client)
938+
939+
ddb_response = {}
940+
stubber.add_response("put_item", ddb_response, expected_params_put_item)
941+
stubber.add_response("update_item", ddb_response, expected_params_update_item)
942+
943+
stubber.activate()
944+
945+
@idempotent(config=idempotency_config, persistence_store=persistence_store)
946+
def lambda_handler(event, context):
947+
return lambda_response
948+
949+
with warnings.catch_warnings(record=True) as w:
950+
warnings.simplefilter("default")
951+
lambda_handler(lambda_apigw_event, lambda_context)
952+
assert len(w) == 1
953+
assert str(w[-1].message) == "Expires in progress is enabled but we couldn't determine the remaining time left"
954+
955+
stubber.assert_no_pending_responses()
956+
stubber.deactivate()
957+
958+
807959
def test_data_record_invalid_status_value():
808960
data_record = DataRecord("key", status="UNSUPPORTED_STATUS")
809961
with pytest.raises(IdempotencyInvalidStatusError) as e:

tests/functional/idempotency/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def build_idempotency_put_item_stub(
2727
"#in_progress_expiry": "in_progress_expiration",
2828
"#status": "status",
2929
},
30-
"ExpressionAttributeValues": {":now": stub.ANY, ":status": "INPROGRESS"},
30+
"ExpressionAttributeValues": {":now": stub.ANY, ":inprogress": "INPROGRESS"},
3131
"Item": {"expiration": stub.ANY, "id": idempotency_key_hash, "status": "INPROGRESS"},
3232
"TableName": "TEST_TABLE",
3333
}

0 commit comments

Comments
 (0)