From 8381d07708ab95689170021cb007a0c1a8ab64bc Mon Sep 17 00:00:00 2001 From: Soujas K Date: Sun, 25 Oct 2026 23:54:16 +0530 Subject: [PATCH] fix(idempotency): warn when a persistence layer is reused with a different config --- .../utilities/idempotency/persistence/base.py | 23 ++++++++++++++++ docs/utilities/idempotency.md | 3 +++ .../idempotency/_boto3/test_idempotency.py | 27 +++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/aws_lambda_powertools/utilities/idempotency/persistence/base.py b/aws_lambda_powertools/utilities/idempotency/persistence/base.py index dcc5c932a9d..498701a1a20 100644 --- a/aws_lambda_powertools/utilities/idempotency/persistence/base.py +++ b/aws_lambda_powertools/utilities/idempotency/persistence/base.py @@ -31,6 +31,7 @@ DataRecord, ) from aws_lambda_powertools.utilities.jmespath_utils import PowertoolsFunctions +from aws_lambda_powertools.warnings import PowertoolsUserWarning if TYPE_CHECKING: from aws_lambda_powertools.utilities.idempotency.config import IdempotencyConfig @@ -73,6 +74,7 @@ def __init__(self): self.expires_after_seconds: int = 60 * 60 # 1 hour default self.use_local_cache = False self.hash_function = hashlib.md5 + self._config_settings: tuple | None = None def configure( self, @@ -96,10 +98,31 @@ def configure( key_prefix or f"{os.getenv(constants.LAMBDA_FUNCTION_NAME_ENV, 'test-func')}.{function_name or ''}" ) + # Settings this layer applies from the config. lambda_context and response_hook are read per call instead. + config_settings = ( + config.event_key_jmespath, + config.payload_validation_jmespath, + config.jmespath_options, + config.raise_on_no_idempotency_key, + config.expires_after_seconds, + config.use_local_cache, + config.local_cache_max_items, + config.hash_function, + ) + if self.configured: # Prevent being reconfigured multiple times + if config_settings != self._config_settings: + warnings.warn( + f"This persistence layer is already configured with a different IdempotencyConfig, so " + f"'{self.function_name}' will use the settings of the first one. " + "Use a separate persistence layer instance for each IdempotencyConfig.", + category=PowertoolsUserWarning, + stacklevel=2, + ) return self.configured = True + self._config_settings = config_settings self.event_key_jmespath = config.event_key_jmespath if config.event_key_jmespath: diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index da7e70ad89c..1b052e3d4a2 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -867,6 +867,9 @@ You can override and further extend idempotency behavior via **`IdempotencyConfi | **hash_function** | `md5` | Function to use for calculating hashes, as provided by [hashlib](https://docs.python.org/3/library/hashlib.html){target="_blank" rel="nofollow"} in the standard library. | | **response_hook** | `None` | Function to use for processing the stored Idempotent response. This function hook is called when an existing idempotent response is found. See [Manipulating The Idempotent Response](idempotency.md#manipulating-the-idempotent-response) | +!!! warning "Use one persistence layer instance per `IdempotencyConfig`" + A persistence layer applies the settings of the first `IdempotencyConfig` it is used with. If functions with different configs share the same persistence layer instance, the later ones use the first config's settings, and we emit a `PowertoolsUserWarning`. + ### Handling concurrent executions with the same payload This utility will raise an **`IdempotencyAlreadyInProgressError`** exception if you receive **multiple invocations with the same payload while the first invocation hasn't completed yet**. diff --git a/tests/functional/idempotency/_boto3/test_idempotency.py b/tests/functional/idempotency/_boto3/test_idempotency.py index fe70251905d..554fda0f640 100644 --- a/tests/functional/idempotency/_boto3/test_idempotency.py +++ b/tests/functional/idempotency/_boto3/test_idempotency.py @@ -1168,6 +1168,33 @@ def test_jmespath_with_powertools_json( assert result == "test-func.handler#" + persistence_store._generate_hash(expected_value) +def test_configure_warns_when_reused_with_a_different_config(persistence_store: DynamoDBPersistenceLayer): + # GIVEN a persistence layer already configured with one IdempotencyConfig + persistence_store.configure(IdempotencyConfig(event_key_jmespath="order_id"), "charge") + + # WHEN it's configured again with a config whose settings differ + with pytest.warns(PowertoolsUserWarning, match="Use a separate persistence layer instance for each"): + persistence_store.configure(IdempotencyConfig(event_key_jmespath="user_id"), "notify") + + # THEN the settings of the first config are kept + assert persistence_store.event_key_jmespath == "order_id" + + +def test_configure_does_not_warn_when_reused_with_the_same_settings(persistence_store: DynamoDBPersistenceLayer): + # GIVEN a persistence layer configured by a function with a shared IdempotencyConfig + shared_config = IdempotencyConfig(event_key_jmespath="order_id") + persistence_store.configure(shared_config, "charge") + + # WHEN another function uses the same config, and a new config with the same settings is passed, + # as @idempotent does per call when no config is given + with warnings.catch_warnings(): + warnings.simplefilter("error", PowertoolsUserWarning) + persistence_store.configure(shared_config, "notify") + persistence_store.configure(IdempotencyConfig(event_key_jmespath="order_id"), "notify") + + # THEN no warning is emitted + + @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,