Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(idempotency): POWERTOOLS_IDEMPOTENCY_DISABLED should respect truthy values #4391

Merged
23 changes: 21 additions & 2 deletions aws_lambda_powertools/utilities/idempotency/idempotency.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import functools
import logging
import os
import warnings
from inspect import isclass
from typing import Any, Callable, Dict, Optional, Type, Union, cast

from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.functions import strtobool
from aws_lambda_powertools.shared.types import AnyCallableT
from aws_lambda_powertools.utilities.idempotency.base import IdempotencyHandler
from aws_lambda_powertools.utilities.idempotency.config import IdempotencyConfig
Expand All @@ -21,6 +23,7 @@
BaseIdempotencySerializer,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from aws_lambda_powertools.warnings import PowertoolsUserWarning

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -66,7 +69,15 @@ def idempotent(
>>> return {"StatusCode": 200}
"""

if os.getenv(constants.IDEMPOTENCY_DISABLED_ENV):
# 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")):
warnings.warn(
message="Disabling idempotency is intended for development environments only "
"and should not be used in production.",
category=PowertoolsUserWarning,
stacklevel=2,
)
return handler(event, context, **kwargs)
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved

config = config or IdempotencyConfig()
Expand Down Expand Up @@ -150,7 +161,15 @@ def process_order(customer_id: str, order: dict, **kwargs):

@functools.wraps(function)
def decorate(*args, **kwargs):
if os.getenv(constants.IDEMPOTENCY_DISABLED_ENV):
# 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")):
warnings.warn(
message="Disabling idempotency is intended for development environments only "
"and should not be used in production.",
category=PowertoolsUserWarning,
stacklevel=2,
)
return function(*args, **kwargs)
leandrodamascena marked this conversation as resolved.
Show resolved Hide resolved

if data_keyword_argument not in kwargs:
Expand Down
23 changes: 23 additions & 0 deletions aws_lambda_powertools/warnings/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Shared warnings that don't belong to a single utility"""


class PowertoolsUserWarning(UserWarning):
"""
This class provides a custom Warning tailored for better clarity when certain situations occur.
leandrodamascena marked this conversation as resolved.
Show resolved Hide resolved

Examples:
- Using development-only features in production environment.
- Potential performance or security issues due to misconfiguration.

Parameters
----------
message: str
The warning message to be displayed.
"""

def __init__(self, message):
self.message = message
super().__init__(message)

def __str__(self):
return self.message
69 changes: 67 additions & 2 deletions tests/functional/idempotency/test_idempotency.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
PydanticSerializer,
)
from aws_lambda_powertools.utilities.validation import envelopes, validator
from aws_lambda_powertools.warnings import PowertoolsUserWarning
from tests.functional.idempotency.utils import (
build_idempotency_put_item_response_stub,
build_idempotency_put_item_stub,
Expand Down Expand Up @@ -1667,13 +1668,20 @@ def dummy(payload):
dummy(payload=data_two)


def test_idempotency_disabled_envvar(monkeypatch, lambda_context, persistence_store: DynamoDBPersistenceLayer):
@pytest.mark.parametrize("idempotency_disabled_value", ["1", "y", "yes", "t", "true", "on"])
def test_idempotency_enabled_envvar_in_dev_environment(
monkeypatch,
lambda_context,
persistence_store: DynamoDBPersistenceLayer,
idempotency_disabled_value,
):
# Scenario to validate no requests sent to dynamodb table when 'POWERTOOLS_IDEMPOTENCY_DISABLED' is set
mock_event = {"data": "value"}

persistence_store.client = MagicMock()

monkeypatch.setenv("POWERTOOLS_IDEMPOTENCY_DISABLED", "1")
monkeypatch.setenv("POWERTOOLS_IDEMPOTENCY_DISABLED", str(idempotency_disabled_value))
monkeypatch.setenv("POWERTOOLS_DEV", "true")

@idempotent_function(data_keyword_argument="data", persistence_store=persistence_store)
def dummy(data):
Expand All @@ -1689,6 +1697,63 @@ def dummy_handler(event, context):
assert len(persistence_store.client.method_calls) == 0


@pytest.mark.parametrize("idempotency_disabled_value", ["1", "y", "yes", "t", "true", "on"])
def test_idempotency_enabled_envvar_in_non_dev_environment(
monkeypatch,
lambda_context,
persistence_store: DynamoDBPersistenceLayer,
idempotency_disabled_value,
):
# Scenario to validate no requests sent to dynamodb table when 'POWERTOOLS_IDEMPOTENCY_DISABLED' is set
mock_event = {"data": "value"}

persistence_store.client = MagicMock()

monkeypatch.setenv("POWERTOOLS_IDEMPOTENCY_DISABLED", str(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"}

with pytest.warns(PowertoolsUserWarning, match="Disabling idempotency is intended for development environments*"):
dummy(data=mock_event)
dummy_handler(mock_event, lambda_context)

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


@pytest.mark.parametrize("idempotency_disabled_value", ["0", "n", "no", "f", "false", "off"])
def test_idempotency_disabled_envvar(
monkeypatch,
lambda_context,
persistence_store: DynamoDBPersistenceLayer,
idempotency_disabled_value,
):
# Scenario to validate no requests sent to dynamodb table when 'POWERTOOLS_IDEMPOTENCY_DISABLED' is false
mock_event = {"data": "value"}

persistence_store.client = MagicMock()

monkeypatch.setenv("POWERTOOLS_IDEMPOTENCY_DISABLED", str(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"}

dummy(data=mock_event)
dummy_handler(mock_event, lambda_context)

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
Loading