-
Notifications
You must be signed in to change notification settings - Fork 408
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
feat(idempotency): adding redis as idempotency backend #1914
Conversation
Thanks a lot for your first contribution! Please check out our contributing guidelines and don't hesitate to ask whatever you need. |
Still working on this before converting it to "Ready for review". |
Codecov ReportBase: 97.44% // Head: 96.24% // Decreases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## develop #1914 +/- ##
===========================================
- Coverage 97.44% 96.24% -1.21%
===========================================
Files 144 149 +5
Lines 6627 6730 +103
Branches 475 482 +7
===========================================
+ Hits 6458 6477 +19
- Misses 132 215 +83
- Partials 37 38 +1
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
Checking in whether we have updates |
Signed-off-by: Vandita Patidar <[email protected]>
Signed-off-by: Vandita Patidar <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Read the code for the first time and left some first remarks. Didn't look too deep into the redis logic itself, just focused on the bigger blocks for now. Great job!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The utilities
folder is used to contain Powertools public utilities, and not internal utilities. Would it make sense to move this under the idempotency
utility folder instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any chance we use the BasePersistenceLayer
for this instead of creating another base class?
See: https://redis.readthedocs.io/en/latest/connections.html#redis.Redis.from_url | ||
""" | ||
|
||
super().__init__(redis.cluster.RedisCluster, host, port, username, password, db_index, url, **extra_options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, the only difference between the two classes is the first parameter (redis.cluster.RedisCluster
vs redis.Redis
).
It that's the only difference, wouldn't it make sense to have a single class with a flag for standalone
vs cluster
instead of 2 different classes?
@@ -37,7 +37,7 @@ class DataRecord: | |||
|
|||
def __init__( | |||
self, | |||
idempotency_key: str, | |||
idempotency_key: str = "", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure we want this? It's probably a breaking change to make this optional.
else: | ||
now = datetime.datetime.now() | ||
period = datetime.timedelta(seconds=self.expires_after_seconds) | ||
return int((now + period).timestamp()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use tell don't ask here. Instead of storing the type of backend, just delegate the get_expiry_timestamp
to the different backends, and let them deal with the details.
@@ -26,7 +26,7 @@ class DynamoDBPersistenceLayer(BasePersistenceLayer): | |||
def __init__( | |||
self, | |||
table_name: str, | |||
key_attr: str = "id", | |||
key_attr: Optional[str] = "id", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably unecessary
if static_pk_value is None: | ||
static_pk_value = f"idempotency#{os.getenv(constants.LAMBDA_FUNCTION_NAME_ENV, '')}" | ||
|
||
self.static_pk_value = static_pk_value | ||
self.in_progress_expiry_attr = in_progress_expiry_attr | ||
self.expiry_attr = expiry_attr | ||
self.status_attr = status_attr | ||
self.data_attr = data_attr | ||
self.validation_key_attr = validation_key_attr | ||
super(RedisCachePersistenceLayer, self).__init__() | ||
|
||
def _get_key(self, idempotency_key: str) -> dict: | ||
# Need to review this after adding GETKEY logic | ||
if self.sort_key_attr: | ||
return {self.key_attr: self.static_pk_value, self.sort_key_attr: idempotency_key} | ||
return {self.key_attr: idempotency_key} | ||
|
||
def _item_to_data_record(self, item: Dict[str, Any]) -> DataRecord: | ||
# Need to review this after adding GETKEY logic | ||
return DataRecord( | ||
status=item[self.status_attr], | ||
expiry_timestamp=item[self.expiry_attr], | ||
in_progress_expiry_timestamp=item.get(self.in_progress_expiry_attr), | ||
response_data=item.get(self.data_attr), | ||
payload_hash=item.get(self.validation_key_attr), | ||
) | ||
|
||
def _get_record(self, idempotency_key) -> DataRecord: | ||
# See: https://redis.io/commands/hgetall/ | ||
response = self._connection.hgetall(idempotency_key) | ||
|
||
try: | ||
item = response | ||
except KeyError: | ||
raise IdempotencyItemNotFoundError | ||
return self._item_to_data_record(item) | ||
|
||
def _put_record(self, data_record: DataRecord) -> None: | ||
# Redis works with hset to support hashing keys with multiple attributes | ||
# See: https://redis.io/commands/hset/ | ||
item = { | ||
"name": data_record.idempotency_key, | ||
"mapping": { | ||
self.in_progress_expiry_attr: data_record.in_progress_expiry_timestamp, | ||
self.status_attr: data_record.status, | ||
self.expiry_attr: data_record.expiry_timestamp, | ||
}, | ||
} | ||
|
||
if data_record.in_progress_expiry_timestamp is not None: | ||
item["mapping"][self.in_progress_expiry_attr] = data_record.in_progress_expiry_timestamp | ||
|
||
if self.payload_validation_enabled: | ||
item["mapping"][self.validation_key_attr] = data_record.payload_hash | ||
|
||
try: | ||
# | LOCKED | RETRY if status = "INPROGRESS" | RETRY | ||
# |----------------|-------------------------------------------------------|-------------> .... (time) | ||
# | Lambda Idempotency Record | ||
# | Timeout Timeout | ||
# | (in_progress_expiry) (expiry) | ||
|
||
# Conditions to successfully save a record: | ||
|
||
# The idempotency key does not exist: | ||
# - first time that this invocation key is used | ||
# - previous invocation with the same key was deleted due to TTL | ||
idempotency_key_not_exist = self._connection.exists(data_record.idempotency_key) | ||
|
||
# key exists | ||
if idempotency_key_not_exist == 1: | ||
raise | ||
|
||
# missing logic to compare expiration | ||
|
||
logger.debug(f"Putting record on Redis for idempotency key: {data_record.idempotency_key}") | ||
self._connection.hset(**item) | ||
# hset type must set expiration after adding the record | ||
# Need to review this to get ttl in seconds | ||
self._connection.expire(name=data_record.idempotency_key, time=self.expires_after_seconds) | ||
except Exception: | ||
logger.debug(f"Failed to put record for already existing idempotency key: {data_record.idempotency_key}") | ||
raise IdempotencyItemAlreadyExistsError | ||
|
||
def _update_record(self, data_record: DataRecord) -> None: | ||
item = { | ||
"name": data_record.idempotency_key, | ||
"mapping": { | ||
self.data_attr: data_record.response_data, | ||
self.status_attr: data_record.status, | ||
}, | ||
} | ||
logger.debug(f"Updating record for idempotency key: {data_record.idempotency_key}") | ||
self._connection.hset(**item) | ||
|
||
def _delete_record(self, data_record: DataRecord) -> None: | ||
logger.debug(f"Deleting record for idempotency key: {data_record.idempotency_key}") | ||
# See: https://redis.io/commands/del/ | ||
self._connection.delete(data_record.idempotency_key) | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops?
@@ -98,12 +99,15 @@ tracer = ["aws-xray-sdk"] | |||
all = ["pydantic", "aws-xray-sdk", "fastjsonschema"] | |||
# allow customers to run code locally without emulators (SAM CLI, etc.) | |||
aws-sdk = ["boto3"] | |||
redis = ["redis"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs to added to all
too on line 98
Closing this PR in favor of #2567! Credit for Vandita's work will be mentioned when we release this feature. |
Issue number: #1181
Summary
PR adds support for Redis as Idempotecny backend
Changes
The following features for Redis are currently supported by this PR
User experience
User can now use Redis as backend. You can connect it using Redis Standalone or Redis Cluster as shown in the example.
Checklist
If your change doesn't seem to apply, please leave them unchecked.
Is this a breaking change?
RFC issue number:
Checklist:
Acknowledgment
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
Disclaimer: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.