12
12
IdempotencyItemAlreadyExistsError ,
13
13
IdempotencyItemNotFoundError ,
14
14
)
15
- from aws_lambda_powertools .utilities .idempotency .persistence .base import DataRecord
15
+ from aws_lambda_powertools .utilities .idempotency .persistence .base import STATUS_CONSTANTS , DataRecord
16
16
17
17
logger = logging .getLogger (__name__ )
18
18
@@ -25,6 +25,7 @@ def __init__(
25
25
static_pk_value : Optional [str ] = None ,
26
26
sort_key_attr : Optional [str ] = None ,
27
27
expiry_attr : str = "expiration" ,
28
+ in_progress_expiry_attr : str = "in_progress_expiration" ,
28
29
status_attr : str = "status" ,
29
30
data_attr : str = "data" ,
30
31
validation_key_attr : str = "validation" ,
@@ -47,6 +48,8 @@ def __init__(
47
48
DynamoDB attribute name for the sort key
48
49
expiry_attr: str, optional
49
50
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"
50
53
status_attr: str, optional
51
54
DynamoDB attribute name for status, by default "status"
52
55
data_attr: str, optional
@@ -85,6 +88,7 @@ def __init__(
85
88
self .static_pk_value = static_pk_value
86
89
self .sort_key_attr = sort_key_attr
87
90
self .expiry_attr = expiry_attr
91
+ self .in_progress_expiry_attr = in_progress_expiry_attr
88
92
self .status_attr = status_attr
89
93
self .data_attr = data_attr
90
94
self .validation_key_attr = validation_key_attr
@@ -133,6 +137,7 @@ def _item_to_data_record(self, item: Dict[str, Any]) -> DataRecord:
133
137
idempotency_key = item [self .key_attr ],
134
138
status = item [self .status_attr ],
135
139
expiry_timestamp = item [self .expiry_attr ],
140
+ in_progress_expiry_timestamp = item .get (self .in_progress_expiry_attr ),
136
141
response_data = item .get (self .data_attr ),
137
142
payload_hash = item .get (self .validation_key_attr ),
138
143
)
@@ -153,33 +158,75 @@ def _put_record(self, data_record: DataRecord) -> None:
153
158
self .status_attr : data_record .status ,
154
159
}
155
160
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
+
156
164
if self .payload_validation_enabled :
157
165
item [self .validation_key_attr ] = data_record .payload_hash
158
166
159
167
now = datetime .datetime .now ()
160
168
try :
161
169
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
+
162
200
self .table .put_item (
163
201
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
+ },
167
214
)
168
215
except self .table .meta .client .exceptions .ConditionalCheckFailedException :
169
216
logger .debug (f"Failed to put record for already existing idempotency key: { data_record .idempotency_key } " )
170
217
raise IdempotencyItemAlreadyExistsError
171
218
172
219
def _update_record (self , data_record : DataRecord ):
173
220
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"
175
222
expression_attr_values = {
176
223
":expiry" : data_record .expiry_timestamp ,
177
224
":response_data" : data_record .response_data ,
178
225
":status" : data_record .status ,
179
226
}
180
227
expression_attr_names = {
181
- "#response_data" : self .data_attr ,
182
228
"#expiry" : self .expiry_attr ,
229
+ "#response_data" : self .data_attr ,
183
230
"#status" : self .status_attr ,
184
231
}
185
232
0 commit comments