diff --git a/aws_lambda_powertools/utilities/idempotency/base.py b/aws_lambda_powertools/utilities/idempotency/base.py index 4b82c923a70..7dee94fc356 100644 --- a/aws_lambda_powertools/utilities/idempotency/base.py +++ b/aws_lambda_powertools/utilities/idempotency/base.py @@ -56,7 +56,7 @@ def __init__( self.fn_args = function_args self.fn_kwargs = function_kwargs - persistence_store.configure(config) + persistence_store.configure(config, self.function.__name__) self.persistence_store = persistence_store def handle(self) -> Any: diff --git a/aws_lambda_powertools/utilities/idempotency/persistence/base.py b/aws_lambda_powertools/utilities/idempotency/persistence/base.py index 907af8edaa7..8f2b30d289a 100644 --- a/aws_lambda_powertools/utilities/idempotency/persistence/base.py +++ b/aws_lambda_powertools/utilities/idempotency/persistence/base.py @@ -112,6 +112,7 @@ class BasePersistenceLayer(ABC): def __init__(self): """Initialize the defaults""" + self.function_name = "" self.configured = False self.event_key_jmespath: Optional[str] = None self.event_key_compiled_jmespath = None @@ -124,7 +125,7 @@ def __init__(self): self._cache: Optional[LRUDict] = None self.hash_function = None - def configure(self, config: IdempotencyConfig) -> None: + def configure(self, config: IdempotencyConfig, function_name: Optional[str] = None) -> None: """ Initialize the base persistence layer from the configuration settings @@ -132,7 +133,11 @@ def configure(self, config: IdempotencyConfig) -> None: ---------- config: IdempotencyConfig Idempotency configuration settings + function_name: str, Optional + The name of the function being decorated """ + self.function_name = f"{os.getenv(constants.LAMBDA_FUNCTION_NAME_ENV, 'test-func')}.{function_name or ''}" + if self.configured: # Prevent being reconfigured multiple times return @@ -178,8 +183,7 @@ def _get_hashed_idempotency_key(self, data: Dict[str, Any]) -> str: warnings.warn(f"No value found for idempotency_key. jmespath: {self.event_key_jmespath}") generated_hash = self._generate_hash(data=data) - function_name = os.getenv(constants.LAMBDA_FUNCTION_NAME_ENV, "test-func") - return f"{function_name}#{generated_hash}" + return f"{self.function_name}#{generated_hash}" @staticmethod def is_missing_idempotency_key(data) -> bool: diff --git a/tests/functional/idempotency/conftest.py b/tests/functional/idempotency/conftest.py index 71b5978497c..0f74d503b88 100644 --- a/tests/functional/idempotency/conftest.py +++ b/tests/functional/idempotency/conftest.py @@ -150,7 +150,7 @@ def expected_params_put_item_with_validation(hashed_idempotency_key, hashed_vali def hashed_idempotency_key(lambda_apigw_event, default_jmespath, lambda_context): compiled_jmespath = jmespath.compile(default_jmespath) data = compiled_jmespath.search(lambda_apigw_event) - return "test-func#" + hashlib.md5(serialize(data).encode()).hexdigest() + return "test-func.lambda_handler#" + hashlib.md5(serialize(data).encode()).hexdigest() @pytest.fixture @@ -158,7 +158,7 @@ def hashed_idempotency_key_with_envelope(lambda_apigw_event): event = extract_data_from_envelope( data=lambda_apigw_event, envelope=envelopes.API_GATEWAY_HTTP, jmespath_options={} ) - return "test-func#" + hashlib.md5(serialize(event).encode()).hexdigest() + return "test-func.lambda_handler#" + hashlib.md5(serialize(event).encode()).hexdigest() @pytest.fixture diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/test_idempotency.py index 043fb06a04a..a8cf652d8a0 100644 --- a/tests/functional/idempotency/test_idempotency.py +++ b/tests/functional/idempotency/test_idempotency.py @@ -735,7 +735,8 @@ def test_default_no_raise_on_missing_idempotency_key( idempotency_config: IdempotencyConfig, persistence_store: DynamoDBPersistenceLayer, lambda_context ): # GIVEN a persistence_store with use_local_cache = False and event_key_jmespath = "body" - persistence_store.configure(idempotency_config) + function_name = "foo" + persistence_store.configure(idempotency_config, function_name) assert persistence_store.use_local_cache is False assert "body" in persistence_store.event_key_jmespath @@ -743,7 +744,7 @@ def test_default_no_raise_on_missing_idempotency_key( hashed_key = persistence_store._get_hashed_idempotency_key({}) # THEN return the hash of None - expected_value = "test-func#" + md5(serialize(None).encode()).hexdigest() + expected_value = f"test-func.{function_name}#" + md5(serialize(None).encode()).hexdigest() assert expected_value == hashed_key @@ -781,7 +782,7 @@ def test_jmespath_with_powertools_json( idempotency_config: IdempotencyConfig, persistence_store: DynamoDBPersistenceLayer, lambda_context ): # GIVEN an event_key_jmespath with powertools_json custom function - persistence_store.configure(idempotency_config) + persistence_store.configure(idempotency_config, "handler") sub_attr_value = "cognito_user" static_pk_value = "some_key" expected_value = [sub_attr_value, static_pk_value] @@ -794,14 +795,14 @@ def test_jmespath_with_powertools_json( result = persistence_store._get_hashed_idempotency_key(api_gateway_proxy_event) # THEN the hashed idempotency key should match the extracted values generated hash - assert result == "test-func#" + persistence_store._generate_hash(expected_value) + assert result == "test-func.handler#" + persistence_store._generate_hash(expected_value) @pytest.mark.parametrize("config_with_jmespath_options", ["powertools_json(data).payload"], indirect=True) def test_custom_jmespath_function_overrides_builtin_functions( config_with_jmespath_options: IdempotencyConfig, persistence_store: DynamoDBPersistenceLayer, lambda_context ): - # GIVEN an persistence store with a custom jmespath_options + # GIVEN a persistence store with a custom jmespath_options # AND use a builtin powertools custom function persistence_store.configure(config_with_jmespath_options) @@ -871,7 +872,9 @@ def _delete_record(self, data_record: DataRecord) -> None: def test_idempotent_lambda_event_source(lambda_context): # Scenario to validate that we can use the event_source decorator before or after the idempotent decorator mock_event = load_event("apiGatewayProxyV2Event.json") - persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()) + persistence_layer = MockPersistenceLayer( + "test-func.lambda_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest() + ) expected_result = {"message": "Foo"} # GIVEN an event_source decorator @@ -891,7 +894,9 @@ def lambda_handler(event, _): def test_idempotent_function(): # Scenario to validate we can use idempotent_function with any function mock_event = {"data": "value"} - persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()) + persistence_layer = MockPersistenceLayer( + "test-func.record_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest() + ) expected_result = {"message": "Foo"} @idempotent_function(persistence_store=persistence_layer, data_keyword_argument="record") @@ -908,7 +913,9 @@ def test_idempotent_function_arbitrary_args_kwargs(): # Scenario to validate we can use idempotent_function with a function # with an arbitrary number of args and kwargs mock_event = {"data": "value"} - persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()) + persistence_layer = MockPersistenceLayer( + "test-func.record_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest() + ) expected_result = {"message": "Foo"} @idempotent_function(persistence_store=persistence_layer, data_keyword_argument="record") @@ -923,7 +930,9 @@ def record_handler(arg_one, arg_two, record, is_record): def test_idempotent_function_invalid_data_kwarg(): mock_event = {"data": "value"} - persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()) + persistence_layer = MockPersistenceLayer( + "test-func.record_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest() + ) expected_result = {"message": "Foo"} keyword_argument = "payload" @@ -940,7 +949,9 @@ def record_handler(record): def test_idempotent_function_arg_instead_of_kwarg(): mock_event = {"data": "value"} - persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()) + persistence_layer = MockPersistenceLayer( + "test-func.record_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest() + ) expected_result = {"message": "Foo"} keyword_argument = "record" @@ -958,13 +969,19 @@ def record_handler(record): def test_idempotent_function_and_lambda_handler(lambda_context): # Scenario to validate we can use both idempotent_function and idempotent decorators mock_event = {"data": "value"} - persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(serialize(mock_event).encode()).hexdigest()) + persistence_layer = MockPersistenceLayer( + "test-func.record_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest() + ) expected_result = {"message": "Foo"} @idempotent_function(persistence_store=persistence_layer, data_keyword_argument="record") def record_handler(record): return expected_result + persistence_layer = MockPersistenceLayer( + "test-func.lambda_handler#" + hashlib.md5(serialize(mock_event).encode()).hexdigest() + ) + @idempotent(persistence_store=persistence_layer) def lambda_handler(event, _): return expected_result @@ -986,7 +1003,9 @@ def test_idempotent_data_sorting(): data_two = {"more_data": "more data 1", "data": "test message 1"} # Assertion will happen in MockPersistenceLayer - persistence_layer = MockPersistenceLayer("test-func#" + hashlib.md5(json.dumps(data_one).encode()).hexdigest()) + persistence_layer = MockPersistenceLayer( + "test-func.dummy#" + hashlib.md5(json.dumps(data_one).encode()).hexdigest() + ) # GIVEN @idempotent_function(data_keyword_argument="payload", persistence_store=persistence_layer) @@ -1017,3 +1036,24 @@ def dummy_handler(event, context): dummy_handler(mock_event, lambda_context) assert len(persistence_store.table.method_calls) == 0 + + +@pytest.mark.parametrize("idempotency_config", [{"use_local_cache": True}], indirect=True) +def test_idempotent_function_duplicates( + idempotency_config: IdempotencyConfig, persistence_store: DynamoDBPersistenceLayer +): + # Scenario to validate the both methods are called + mock_event = {"data": "value"} + persistence_store.table = MagicMock() + + @idempotent_function(data_keyword_argument="data", persistence_store=persistence_store, config=idempotency_config) + def one(data): + return "one" + + @idempotent_function(data_keyword_argument="data", persistence_store=persistence_store, config=idempotency_config) + def two(data): + return "two" + + assert one(data=mock_event) == "one" + assert two(data=mock_event) == "two" + assert len(persistence_store.table.method_calls) == 4