Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions aws_lambda_powertools/utilities/idempotency/persistence/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions docs/utilities/idempotency.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand Down
27 changes: 27 additions & 0 deletions tests/functional/idempotency/_boto3/test_idempotency.py
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,33 @@
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"):

Check warning on line 1176 in tests/functional/idempotency/_boto3/test_idempotency.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this warning test to have only one invocation possibly emitting a warning.

See more on https://sonarcloud.io/project/issues?id=aws-powertools_powertools-lambda-python&issues=AaF0V81BY4cSrRUGOf2t&open=AaF0V81BY4cSrRUGOf2t&pullRequest=8664
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,
Expand Down