Skip to content

Commit 5cb9852

Browse files
author
Michael Brewer
authored
fix(idempotency): include decorated fn name in hash (#869)
1 parent 81fc02e commit 5cb9852

File tree

4 files changed

+62
-18
lines changed

4 files changed

+62
-18
lines changed

Diff for: aws_lambda_powertools/utilities/idempotency/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(
5656
self.fn_args = function_args
5757
self.fn_kwargs = function_kwargs
5858

59-
persistence_store.configure(config)
59+
persistence_store.configure(config, self.function.__name__)
6060
self.persistence_store = persistence_store
6161

6262
def handle(self) -> Any:

Diff for: aws_lambda_powertools/utilities/idempotency/persistence/base.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ class BasePersistenceLayer(ABC):
112112

113113
def __init__(self):
114114
"""Initialize the defaults"""
115+
self.function_name = ""
115116
self.configured = False
116117
self.event_key_jmespath: Optional[str] = None
117118
self.event_key_compiled_jmespath = None
@@ -124,15 +125,19 @@ def __init__(self):
124125
self._cache: Optional[LRUDict] = None
125126
self.hash_function = None
126127

127-
def configure(self, config: IdempotencyConfig) -> None:
128+
def configure(self, config: IdempotencyConfig, function_name: Optional[str] = None) -> None:
128129
"""
129130
Initialize the base persistence layer from the configuration settings
130131
131132
Parameters
132133
----------
133134
config: IdempotencyConfig
134135
Idempotency configuration settings
136+
function_name: str, Optional
137+
The name of the function being decorated
135138
"""
139+
self.function_name = f"{os.getenv(constants.LAMBDA_FUNCTION_NAME_ENV, 'test-func')}.{function_name or ''}"
140+
136141
if self.configured:
137142
# Prevent being reconfigured multiple times
138143
return
@@ -178,8 +183,7 @@ def _get_hashed_idempotency_key(self, data: Dict[str, Any]) -> str:
178183
warnings.warn(f"No value found for idempotency_key. jmespath: {self.event_key_jmespath}")
179184

180185
generated_hash = self._generate_hash(data=data)
181-
function_name = os.getenv(constants.LAMBDA_FUNCTION_NAME_ENV, "test-func")
182-
return f"{function_name}#{generated_hash}"
186+
return f"{self.function_name}#{generated_hash}"
183187

184188
@staticmethod
185189
def is_missing_idempotency_key(data) -> bool:

Diff for: tests/functional/idempotency/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,15 @@ def expected_params_put_item_with_validation(hashed_idempotency_key, hashed_vali
150150
def hashed_idempotency_key(lambda_apigw_event, default_jmespath, lambda_context):
151151
compiled_jmespath = jmespath.compile(default_jmespath)
152152
data = compiled_jmespath.search(lambda_apigw_event)
153-
return "test-func#" + hashlib.md5(serialize(data).encode()).hexdigest()
153+
return "test-func.lambda_handler#" + hashlib.md5(serialize(data).encode()).hexdigest()
154154

155155

156156
@pytest.fixture
157157
def hashed_idempotency_key_with_envelope(lambda_apigw_event):
158158
event = extract_data_from_envelope(
159159
data=lambda_apigw_event, envelope=envelopes.API_GATEWAY_HTTP, jmespath_options={}
160160
)
161-
return "test-func#" + hashlib.md5(serialize(event).encode()).hexdigest()
161+
return "test-func.lambda_handler#" + hashlib.md5(serialize(event).encode()).hexdigest()
162162

163163

164164
@pytest.fixture

Diff for: tests/functional/idempotency/test_idempotency.py

+52-12
Original file line numberDiff line numberDiff line change
@@ -735,15 +735,16 @@ def test_default_no_raise_on_missing_idempotency_key(
735735
idempotency_config: IdempotencyConfig, persistence_store: DynamoDBPersistenceLayer, lambda_context
736736
):
737737
# GIVEN a persistence_store with use_local_cache = False and event_key_jmespath = "body"
738-
persistence_store.configure(idempotency_config)
738+
function_name = "foo"
739+
persistence_store.configure(idempotency_config, function_name)
739740
assert persistence_store.use_local_cache is False
740741
assert "body" in persistence_store.event_key_jmespath
741742

742743
# WHEN getting the hashed idempotency key for an event with no `body` key
743744
hashed_key = persistence_store._get_hashed_idempotency_key({})
744745

745746
# THEN return the hash of None
746-
expected_value = "test-func#" + md5(serialize(None).encode()).hexdigest()
747+
expected_value = f"test-func.{function_name}#" + md5(serialize(None).encode()).hexdigest()
747748
assert expected_value == hashed_key
748749

749750

@@ -781,7 +782,7 @@ def test_jmespath_with_powertools_json(
781782
idempotency_config: IdempotencyConfig, persistence_store: DynamoDBPersistenceLayer, lambda_context
782783
):
783784
# GIVEN an event_key_jmespath with powertools_json custom function
784-
persistence_store.configure(idempotency_config)
785+
persistence_store.configure(idempotency_config, "handler")
785786
sub_attr_value = "cognito_user"
786787
static_pk_value = "some_key"
787788
expected_value = [sub_attr_value, static_pk_value]
@@ -794,14 +795,14 @@ def test_jmespath_with_powertools_json(
794795
result = persistence_store._get_hashed_idempotency_key(api_gateway_proxy_event)
795796

796797
# THEN the hashed idempotency key should match the extracted values generated hash
797-
assert result == "test-func#" + persistence_store._generate_hash(expected_value)
798+
assert result == "test-func.handler#" + persistence_store._generate_hash(expected_value)
798799

799800

800801
@pytest.mark.parametrize("config_with_jmespath_options", ["powertools_json(data).payload"], indirect=True)
801802
def test_custom_jmespath_function_overrides_builtin_functions(
802803
config_with_jmespath_options: IdempotencyConfig, persistence_store: DynamoDBPersistenceLayer, lambda_context
803804
):
804-
# GIVEN an persistence store with a custom jmespath_options
805+
# GIVEN a persistence store with a custom jmespath_options
805806
# AND use a builtin powertools custom function
806807
persistence_store.configure(config_with_jmespath_options)
807808

@@ -871,7 +872,9 @@ def _delete_record(self, data_record: DataRecord) -> None:
871872
def test_idempotent_lambda_event_source(lambda_context):
872873
# Scenario to validate that we can use the event_source decorator before or after the idempotent decorator
873874
mock_event = load_event("apiGatewayProxyV2Event.json")
874-
persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest())
875+
persistence_layer = MockPersistenceLayer(
876+
"test-func.lambda_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()
877+
)
875878
expected_result = {"message": "Foo"}
876879

877880
# GIVEN an event_source decorator
@@ -891,7 +894,9 @@ def lambda_handler(event, _):
891894
def test_idempotent_function():
892895
# Scenario to validate we can use idempotent_function with any function
893896
mock_event = {"data": "value"}
894-
persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest())
897+
persistence_layer = MockPersistenceLayer(
898+
"test-func.record_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()
899+
)
895900
expected_result = {"message": "Foo"}
896901

897902
@idempotent_function(persistence_store=persistence_layer, data_keyword_argument="record")
@@ -908,7 +913,9 @@ def test_idempotent_function_arbitrary_args_kwargs():
908913
# Scenario to validate we can use idempotent_function with a function
909914
# with an arbitrary number of args and kwargs
910915
mock_event = {"data": "value"}
911-
persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest())
916+
persistence_layer = MockPersistenceLayer(
917+
"test-func.record_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()
918+
)
912919
expected_result = {"message": "Foo"}
913920

914921
@idempotent_function(persistence_store=persistence_layer, data_keyword_argument="record")
@@ -923,7 +930,9 @@ def record_handler(arg_one, arg_two, record, is_record):
923930

924931
def test_idempotent_function_invalid_data_kwarg():
925932
mock_event = {"data": "value"}
926-
persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest())
933+
persistence_layer = MockPersistenceLayer(
934+
"test-func.record_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()
935+
)
927936
expected_result = {"message": "Foo"}
928937
keyword_argument = "payload"
929938

@@ -940,7 +949,9 @@ def record_handler(record):
940949

941950
def test_idempotent_function_arg_instead_of_kwarg():
942951
mock_event = {"data": "value"}
943-
persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest())
952+
persistence_layer = MockPersistenceLayer(
953+
"test-func.record_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()
954+
)
944955
expected_result = {"message": "Foo"}
945956
keyword_argument = "record"
946957

@@ -958,13 +969,19 @@ def record_handler(record):
958969
def test_idempotent_function_and_lambda_handler(lambda_context):
959970
# Scenario to validate we can use both idempotent_function and idempotent decorators
960971
mock_event = {"data": "value"}
961-
persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest())
972+
persistence_layer = MockPersistenceLayer(
973+
"test-func.record_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()
974+
)
962975
expected_result = {"message": "Foo"}
963976

964977
@idempotent_function(persistence_store=persistence_layer, data_keyword_argument="record")
965978
def record_handler(record):
966979
return expected_result
967980

981+
persistence_layer = MockPersistenceLayer(
982+
"test-func.lambda_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()
983+
)
984+
968985
@idempotent(persistence_store=persistence_layer)
969986
def lambda_handler(event, _):
970987
return expected_result
@@ -986,7 +1003,9 @@ def test_idempotent_data_sorting():
9861003
data_two = {"more_data": "more data 1", "data": "test message 1"}
9871004

9881005
# Assertion will happen in MockPersistenceLayer
989-
persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(json.dumps(data_one).encode()).hexdigest())
1006+
persistence_layer = MockPersistenceLayer(
1007+
"test-func.dummy#" + hashlib.md5(json.dumps(data_one).encode()).hexdigest()
1008+
)
9901009

9911010
# GIVEN
9921011
@idempotent_function(data_keyword_argument="payload", persistence_store=persistence_layer)
@@ -1017,3 +1036,24 @@ def dummy_handler(event, context):
10171036
dummy_handler(mock_event, lambda_context)
10181037

10191038
assert len(persistence_store.table.method_calls) == 0
1039+
1040+
1041+
@pytest.mark.parametrize("idempotency_config", [{"use_local_cache": True}], indirect=True)
1042+
def test_idempotent_function_duplicates(
1043+
idempotency_config: IdempotencyConfig, persistence_store: DynamoDBPersistenceLayer
1044+
):
1045+
# Scenario to validate the both methods are called
1046+
mock_event = {"data": "value"}
1047+
persistence_store.table = MagicMock()
1048+
1049+
@idempotent_function(data_keyword_argument="data", persistence_store=persistence_store, config=idempotency_config)
1050+
def one(data):
1051+
return "one"
1052+
1053+
@idempotent_function(data_keyword_argument="data", persistence_store=persistence_store, config=idempotency_config)
1054+
def two(data):
1055+
return "two"
1056+
1057+
assert one(data=mock_event) == "one"
1058+
assert two(data=mock_event) == "two"
1059+
assert len(persistence_store.table.method_calls) == 4

0 commit comments

Comments
 (0)