-
Notifications
You must be signed in to change notification settings - Fork 429
feat: Idempotency helper utility #245
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
Merged
Merged
Changes from 12 commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
d503fb0
feat: initial commit for idempotency utility
e564c63
fix: ensure region is configured in botocore for tests
c4d19ba
chore: ignore security warning for md5 usage
45d384b
chore: add debug logging
13e5b09
feat: add local caching for idempotency lookups
1efc27d
feat: replace simple dict cache with LRU
546e879
feat: remove idempotent exception handling
60fd336
feat: remove unused logic to create ddb table - will handle in docume…
ee46124
fix: remove redundant code from table creation logic
acab091
chore: move tests to own dir
2a364fd
chore: remove redundant code for exception handling
4f5d52b
feat: add payload validation logic and functionality to use different…
a19d955
feat: optimization to reduce number of database calls, reorganize per…
0ef52f9
chore: type corrections
d128b0a
chore: add more logging statements
4caa52c
fix: Use variable for ddb attribute name
d89fcee
chore: clarify docstring for abstract method
ed9e0c2
feat: Refactor to cover corner cases where state changes between call…
7000927
chore: correct stubbed ddb responses for test case
c4856fd
docs: add first of a few seq diagrams to support documentation
aed4a7b
feat: use boto3 session for constructing clients to allow customizati…
3b6c2e3
Merge branch 'develop' into feat/idempotency_helper
2047d34
chore: move cache dict implementation to shared dir
8a054cb
chore: refactor with improvements for readability, variable names, an…
523535f
chore: remove dead code, rename variable for clarity, change args to …
834db1c
chore: improve test coverage, refactor fixtures
24f6187
Merge branch 'develop' into feat/idempotency_helper
41d559e
chore: skip tests using pytest-mock's spy for python < 3.8 due to iss…
dca02ee
Merge branch 'develop' into feat/idempotency_helper
b4490b9
chore: update test fixtures to use jmespath
43b72e7
docs: first draft of docs for idempotency util
88e983e
fix: Allow event_key_jmespath to be left empty to use entire event as…
d17275d
docs: add section for compatibility with other utils
83d78ce
chore: improvements to func tests
4bdfdf6
chore: add unit tests for lru cache
9de6e29
feat: add support for decimals in json serializer
a4cc61a
chore: Add docstring for LRU cache dict
978a6bb
chore: Remove unused status constants
1fd8b5a
chore: Rename method for clarity
022739e
chore: Correct example in docstring
8a2d4fe
fix: make data attribute of data record optional in get_record so we …
e6f2d98
docs: clarify behaviour for concurrent executions and DDB behaviour f…
4aa8145
Update aws_lambda_powertools/shared/cache_dict.py
c54952c
Update aws_lambda_powertools/shared/cache_dict.py
7832e56
Update aws_lambda_powertools/utilities/idempotency/persistence/base.py
fed58fe
chore: add test for invalid status on data record
b079387
Update aws_lambda_powertools/utilities/idempotency/persistence/base.py
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
""" | ||
Utility for adding idempotency to lambda functions | ||
""" | ||
|
||
from .idempotency import idempotent | ||
from .persistence import BasePersistenceLayer, DynamoDBPersistenceLayer | ||
|
||
__all__ = ("DynamoDBPersistenceLayer", "BasePersistenceLayer", "idempotent") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from collections import OrderedDict | ||
|
||
|
||
class LRUDict(OrderedDict): | ||
def __init__(self, max_size=1024, *args, **kwds): | ||
self.max_size = max_size | ||
super().__init__(*args, **kwds) | ||
|
||
def __getitem__(self, key): | ||
value = super().__getitem__(key) | ||
self.move_to_end(key) | ||
return value | ||
|
||
def __setitem__(self, key, value): | ||
if key in self: | ||
self.move_to_end(key) | ||
super().__setitem__(key, value) | ||
if len(self) > self.max_size: | ||
oldest = next(iter(self)) | ||
del self[oldest] | ||
|
||
def get(self, key, *args, **kwargs): | ||
item = super(LRUDict, self).get(key, *args, **kwargs) | ||
if item: | ||
self.move_to_end(key=key) | ||
return item |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
""" | ||
Idempotency errors | ||
""" | ||
|
||
|
||
class ItemAlreadyExistsError(Exception): | ||
""" | ||
Item attempting to be inserted into persistence store already exists | ||
""" | ||
|
||
|
||
class ItemNotFoundError(Exception): | ||
""" | ||
Item does not exist in persistence store | ||
""" | ||
|
||
|
||
class AlreadyInProgressError(Exception): | ||
""" | ||
Execution with idempotency key is already in progress | ||
""" | ||
|
||
|
||
class InvalidStatusError(Exception): | ||
""" | ||
An invalid status was provided | ||
""" | ||
|
||
|
||
class IdempotencyValidationerror(Exception): | ||
""" | ||
Payload does not match stored idempotency record | ||
""" |
96 changes: 96 additions & 0 deletions
96
aws_lambda_powertools/utilities/idempotency/idempotency.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
""" | ||
Primary interface for idempotent Lambda functions utility | ||
""" | ||
import logging | ||
from typing import Any, Callable, Dict | ||
|
||
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator | ||
|
||
from ..typing import LambdaContext | ||
from .exceptions import AlreadyInProgressError, ItemNotFoundError | ||
from .persistence import STATUS_CONSTANTS, BasePersistenceLayer | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def default_error_callback(): | ||
raise | ||
|
||
|
||
@lambda_handler_decorator | ||
def idempotent( | ||
handler: Callable[[Any, LambdaContext], Any], | ||
event: Dict[str, Any], | ||
context: LambdaContext, | ||
persistence: BasePersistenceLayer, | ||
) -> Any: | ||
""" | ||
Middleware to handle idempotency | ||
|
||
Parameters | ||
---------- | ||
handler: Callable | ||
Lambda's handler | ||
event: Dict | ||
Lambda's Event | ||
context: Dict | ||
Lambda's Context | ||
persistence: BasePersistenceLayer | ||
Instance of BasePersistenceLayer to store data | ||
|
||
Examples | ||
-------- | ||
**Processes Lambda's event in an idempotent manner** | ||
>>> from aws_lambda_powertools.utilities.idempotency import idempotent, DynamoDBPersistenceLayer | ||
>>> | ||
>>> persistence_store = DynamoDBPersistenceLayer(event_key="body", table_name="idempotency_store") | ||
>>> | ||
>>> @idempotent(persistence=persistence_store) | ||
>>> def handler(event, context): | ||
>>> return {"StatusCode": 200} | ||
""" | ||
|
||
persistence_instance = persistence | ||
try: | ||
to-mc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
event_record = persistence_instance.get_record(event) | ||
to-mc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
except ItemNotFoundError: | ||
persistence_instance.save_inprogress(event=event) | ||
to-mc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return _call_lambda(handler=handler, persistence_instance=persistence_instance, event=event, context=context) | ||
|
||
if event_record.status == STATUS_CONSTANTS["EXPIRED"]: | ||
return _call_lambda(handler=handler, persistence_instance=persistence_instance, event=event, context=context) | ||
|
||
if event_record.status == STATUS_CONSTANTS["INPROGRESS"]: | ||
raise AlreadyInProgressError( | ||
f"Execution already in progress with idempotency key: " | ||
f"{persistence_instance.event_key}={event_record.idempotency_key}" | ||
) | ||
|
||
if event_record.status == STATUS_CONSTANTS["COMPLETED"]: | ||
return event_record.response_json_as_dict() | ||
|
||
|
||
def _call_lambda( | ||
handler: Callable, persistence_instance: BasePersistenceLayer, event: Dict[str, Any], context: LambdaContext | ||
) -> Any: | ||
""" | ||
|
||
Parameters | ||
---------- | ||
handler: Callable | ||
Lambda handler | ||
persistence_instance: BasePersistenceLayer | ||
Instance of persistence layer | ||
event | ||
Lambda event | ||
context | ||
Lambda context | ||
""" | ||
try: | ||
handler_response = handler(event, context) | ||
to-mc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
except Exception as ex: | ||
persistence_instance.save_error(event=event, exception=ex) | ||
raise | ||
else: | ||
persistence_instance.save_success(event=event, result=handler_response) | ||
return handler_response |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.