1212 IdempotencyItemAlreadyExistsError ,
1313 IdempotencyItemNotFoundError ,
1414)
15- from aws_lambda_powertools .utilities .idempotency .persistence .base import DataRecord
15+ from aws_lambda_powertools .utilities .idempotency .persistence .base import STATUS_CONSTANTS , DataRecord
1616
1717logger = logging .getLogger (__name__ )
1818
@@ -25,6 +25,7 @@ def __init__(
2525 static_pk_value : Optional [str ] = None ,
2626 sort_key_attr : Optional [str ] = None ,
2727 expiry_attr : str = "expiration" ,
28+ in_progress_expiry_attr : str = "in_progress_expiration" ,
2829 status_attr : str = "status" ,
2930 data_attr : str = "data" ,
3031 validation_key_attr : str = "validation" ,
@@ -47,6 +48,8 @@ def __init__(
4748 DynamoDB attribute name for the sort key
4849 expiry_attr: str, optional
4950 DynamoDB attribute name for expiry timestamp, by default "expiration"
51+ in_progress_expiry_attr: str, optional
52+ DynamoDB attribute name for in-progress expiry timestamp, by default "in_progress_expiration"
5053 status_attr: str, optional
5154 DynamoDB attribute name for status, by default "status"
5255 data_attr: str, optional
@@ -85,6 +88,7 @@ def __init__(
8588 self .static_pk_value = static_pk_value
8689 self .sort_key_attr = sort_key_attr
8790 self .expiry_attr = expiry_attr
91+ self .in_progress_expiry_attr = in_progress_expiry_attr
8892 self .status_attr = status_attr
8993 self .data_attr = data_attr
9094 self .validation_key_attr = validation_key_attr
@@ -133,6 +137,7 @@ def _item_to_data_record(self, item: Dict[str, Any]) -> DataRecord:
133137 idempotency_key = item [self .key_attr ],
134138 status = item [self .status_attr ],
135139 expiry_timestamp = item [self .expiry_attr ],
140+ in_progress_expiry_timestamp = item .get (self .in_progress_expiry_attr ),
136141 response_data = item .get (self .data_attr ),
137142 payload_hash = item .get (self .validation_key_attr ),
138143 )
@@ -153,33 +158,75 @@ def _put_record(self, data_record: DataRecord) -> None:
153158 self .status_attr : data_record .status ,
154159 }
155160
161+ if data_record .in_progress_expiry_timestamp is not None :
162+ item [self .in_progress_expiry_attr ] = data_record .in_progress_expiry_timestamp
163+
156164 if self .payload_validation_enabled :
157165 item [self .validation_key_attr ] = data_record .payload_hash
158166
159167 now = datetime .datetime .now ()
160168 try :
161169 logger .debug (f"Putting record for idempotency key: { data_record .idempotency_key } " )
170+
171+ # | LOCKED | RETRY if status = "INPROGRESS" | RETRY
172+ # |----------------|-------------------------------------------------------|-------------> .... (time)
173+ # | Lambda Idempotency Record
174+ # | Timeout Timeout
175+ # | (in_progress_expiry) (expiry)
176+
177+ # Conditions to successfully save a record:
178+
179+ # The idempotency key does not exist:
180+ # - first time that this invocation key is used
181+ # - previous invocation with the same key was deleted due to TTL
182+ idempotency_key_not_exist = "attribute_not_exists(#id)"
183+
184+ # The idempotency record exists but it's expired:
185+ idempotency_expiry_expired = "#expiry < :now"
186+
187+ # The status of the record is "INPROGRESS", there is an in-progress expiry timestamp, but it's expired
188+ inprogress_expiry_expired = " AND " .join (
189+ [
190+ "#status = :inprogress" ,
191+ "attribute_exists(#in_progress_expiry)" ,
192+ "#in_progress_expiry < :now_in_millis" ,
193+ ]
194+ )
195+
196+ condition_expression = (
197+ f"{ idempotency_key_not_exist } OR { idempotency_expiry_expired } OR ({ inprogress_expiry_expired } )"
198+ )
199+
162200 self .table .put_item (
163201 Item = item ,
164- ConditionExpression = "attribute_not_exists(#id) OR #now < :now" ,
165- ExpressionAttributeNames = {"#id" : self .key_attr , "#now" : self .expiry_attr },
166- ExpressionAttributeValues = {":now" : int (now .timestamp ())},
202+ ConditionExpression = condition_expression ,
203+ ExpressionAttributeNames = {
204+ "#id" : self .key_attr ,
205+ "#expiry" : self .expiry_attr ,
206+ "#in_progress_expiry" : self .in_progress_expiry_attr ,
207+ "#status" : self .status_attr ,
208+ },
209+ ExpressionAttributeValues = {
210+ ":now" : int (now .timestamp ()),
211+ ":now_in_millis" : int (now .timestamp () * 1000 ),
212+ ":inprogress" : STATUS_CONSTANTS ["INPROGRESS" ],
213+ },
167214 )
168215 except self .table .meta .client .exceptions .ConditionalCheckFailedException :
169216 logger .debug (f"Failed to put record for already existing idempotency key: { data_record .idempotency_key } " )
170217 raise IdempotencyItemAlreadyExistsError
171218
172219 def _update_record (self , data_record : DataRecord ):
173220 logger .debug (f"Updating record for idempotency key: { data_record .idempotency_key } " )
174- update_expression = "SET #response_data = :response_data, #expiry = :expiry, #status = :status"
221+ update_expression = "SET #response_data = :response_data, #expiry = :expiry, " " #status = :status"
175222 expression_attr_values = {
176223 ":expiry" : data_record .expiry_timestamp ,
177224 ":response_data" : data_record .response_data ,
178225 ":status" : data_record .status ,
179226 }
180227 expression_attr_names = {
181- "#response_data" : self .data_attr ,
182228 "#expiry" : self .expiry_attr ,
229+ "#response_data" : self .data_attr ,
183230 "#status" : self .status_attr ,
184231 }
185232
0 commit comments