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
18 changes: 16 additions & 2 deletions aws_lambda_powertools/utilities/idempotency/idempotency.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@
ContextT = TypeVar("ContextT", bound="LambdaContext | DurableContextProtocol")


def _is_idempotency_disabled() -> bool:
# An empty or unparsable value is easy to end up with in IaC templates and CI variables;
# treat it as "not disabled" instead of failing every invocation
value = os.getenv(constants.IDEMPOTENCY_DISABLED_ENV, "false")
try:
return strtobool(value)
except ValueError:
logger.debug(
f"Ignoring {constants.IDEMPOTENCY_DISABLED_ENV}={value!r}: "
"not a recognised boolean, idempotency stays enabled",
)
return False


class _IdempotentHandlerDecorator(Protocol):
# The return type is Any, as responses replayed from the persistence store are deserialized JSON
def __call__(
Expand Down Expand Up @@ -106,7 +120,7 @@ def handler(event, context):

# Skip idempotency controls when POWERTOOLS_IDEMPOTENCY_DISABLED has a truthy value
# Raises a warning if not running in development mode
if strtobool(os.getenv(constants.IDEMPOTENCY_DISABLED_ENV, "false")):
if _is_idempotency_disabled():
warnings.warn(
message="Disabling idempotency is intended for development environments only "
"and should not be used in production.",
Expand Down Expand Up @@ -216,7 +230,7 @@ def process_order(customer_id: str, order: dict, **kwargs):
def decorate(*args, **kwargs):
# Skip idempotency controls when POWERTOOLS_IDEMPOTENCY_DISABLED has a truthy value
# Raises a warning if not running in development mode
if strtobool(os.getenv(constants.IDEMPOTENCY_DISABLED_ENV, "false")):
if _is_idempotency_disabled():
warnings.warn(
message="Disabling idempotency is intended for development environments only "
"and should not be used in production.",
Expand Down
32 changes: 32 additions & 0 deletions tests/functional/idempotency/_boto3/test_idempotency.py
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,38 @@ def dummy_handler(event, context):
assert len(persistence_store.client.method_calls) == 4


@pytest.mark.parametrize("idempotency_disabled_value", ["", " ", "maybe", None])
def test_idempotency_disabled_envvar_empty_or_unparsable(
monkeypatch,
lambda_context,
persistence_store: DynamoDBPersistenceLayer,
idempotency_disabled_value,
):
# Scenario to validate an empty, unset or unparsable 'POWERTOOLS_IDEMPOTENCY_DISABLED'
# keeps idempotency enabled instead of crashing the invocation
mock_event = {"data": "value"}

persistence_store.client = MagicMock()

if idempotency_disabled_value is None:
monkeypatch.delenv("POWERTOOLS_IDEMPOTENCY_DISABLED", raising=False)
else:
monkeypatch.setenv("POWERTOOLS_IDEMPOTENCY_DISABLED", idempotency_disabled_value)

@idempotent_function(data_keyword_argument="data", persistence_store=persistence_store)
def dummy(data):
return {"message": "hello"}

@idempotent(persistence_store=persistence_store)
def dummy_handler(event, context):
return {"message": "hi"}

assert dummy(data=mock_event) == {"message": "hello"}
assert dummy_handler(mock_event, lambda_context) == {"message": "hi"}

assert len(persistence_store.client.method_calls) == 4


@pytest.mark.parametrize("idempotency_config", [{"use_local_cache": True}], indirect=True)
def test_idempotent_function_duplicates(
idempotency_config: IdempotencyConfig,
Expand Down