Skip to content

Commit 5daa45a

Browse files
Michael BrewerJoris Conijn
Michael Brewer
and
Joris Conijn
authored
feat(idempotent): Include function name in the idempotent key (#326)
* feat(idempotent): Prefix key with function name * refactor: Use formatted string * chore: Apply code review change Co-authored-by: Joris Conijn <[email protected]> * chore: Apply code review changes Co-authored-by: Joris Conijn <[email protected]> * feat(idempotent): Add include_function_name option * chore: Fix variable typo * refactor(idempotent): remove include_function_name * chore: remove unused param Co-authored-by: Joris Conijn <[email protected]>
1 parent 34c2fa9 commit 5daa45a

File tree

4 files changed

+90
-49
lines changed

4 files changed

+90
-49
lines changed

Diff for: aws_lambda_powertools/utilities/idempotency/idempotency.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def handle(self) -> Any:
131131
try:
132132
# We call save_inprogress first as an optimization for the most common case where no idempotent record
133133
# already exists. If it succeeds, there's no need to call get_record.
134-
self.persistence_store.save_inprogress(event=self.event)
134+
self.persistence_store.save_inprogress(event=self.event, context=self.context)
135135
except IdempotencyItemAlreadyExistsError:
136136
# Now we know the item already exists, we can retrieve it
137137
record = self._get_idempotency_record()
@@ -151,7 +151,7 @@ def _get_idempotency_record(self) -> DataRecord:
151151
152152
"""
153153
try:
154-
event_record = self.persistence_store.get_record(self.event)
154+
event_record = self.persistence_store.get_record(event=self.event, context=self.context)
155155
except IdempotencyItemNotFoundError:
156156
# This code path will only be triggered if the record is removed between save_inprogress and get_record.
157157
logger.debug(
@@ -219,7 +219,9 @@ def _call_lambda_handler(self) -> Any:
219219
# We need these nested blocks to preserve lambda handler exception in case the persistence store operation
220220
# also raises an exception
221221
try:
222-
self.persistence_store.delete_record(event=self.event, exception=handler_exception)
222+
self.persistence_store.delete_record(
223+
event=self.event, context=self.context, exception=handler_exception
224+
)
223225
except Exception as delete_exception:
224226
raise IdempotencyPersistenceLayerError(
225227
"Failed to delete record from idempotency store"
@@ -228,7 +230,7 @@ def _call_lambda_handler(self) -> Any:
228230

229231
else:
230232
try:
231-
self.persistence_store.save_success(event=self.event, result=handler_response)
233+
self.persistence_store.save_success(event=self.event, context=self.context, result=handler_response)
232234
except Exception as save_exception:
233235
raise IdempotencyPersistenceLayerError(
234236
"Failed to update record state to success in idempotency store"

Diff for: aws_lambda_powertools/utilities/idempotency/persistence/base.py

+26-15
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
IdempotencyKeyError,
2424
IdempotencyValidationError,
2525
)
26+
from aws_lambda_powertools.utilities.typing import LambdaContext
2627

2728
logger = logging.getLogger(__name__)
2829

@@ -152,34 +153,35 @@ def configure(self, config: IdempotencyConfig) -> None:
152153
self._cache = LRUDict(max_items=config.local_cache_max_items)
153154
self.hash_function = getattr(hashlib, config.hash_function)
154155

155-
def _get_hashed_idempotency_key(self, lambda_event: Dict[str, Any]) -> str:
156+
def _get_hashed_idempotency_key(self, event: Dict[str, Any], context: LambdaContext) -> str:
156157
"""
157158
Extract data from lambda event using event key jmespath, and return a hashed representation
158159
159160
Parameters
160161
----------
161-
lambda_event: Dict[str, Any]
162+
event: Dict[str, Any]
162163
Lambda event
164+
context: LambdaContext
165+
Lambda context
163166
164167
Returns
165168
-------
166169
str
167170
Hashed representation of the data extracted by the jmespath expression
168171
169172
"""
170-
data = lambda_event
173+
data = event
171174

172175
if self.event_key_jmespath:
173-
data = self.event_key_compiled_jmespath.search(
174-
lambda_event, options=jmespath.Options(**self.jmespath_options)
175-
)
176+
data = self.event_key_compiled_jmespath.search(event, options=jmespath.Options(**self.jmespath_options))
176177

177178
if self.is_missing_idempotency_key(data):
178179
if self.raise_on_no_idempotency_key:
179180
raise IdempotencyKeyError("No data found to create a hashed idempotency_key")
180181
warnings.warn(f"No value found for idempotency_key. jmespath: {self.event_key_jmespath}")
181182

182-
return self._generate_hash(data)
183+
generated_hash = self._generate_hash(data)
184+
return f"{context.function_name}#{generated_hash}"
183185

184186
@staticmethod
185187
def is_missing_idempotency_key(data) -> bool:
@@ -298,21 +300,23 @@ def _delete_from_cache(self, idempotency_key: str):
298300
if idempotency_key in self._cache:
299301
del self._cache[idempotency_key]
300302

301-
def save_success(self, event: Dict[str, Any], result: dict) -> None:
303+
def save_success(self, event: Dict[str, Any], context: LambdaContext, result: dict) -> None:
302304
"""
303305
Save record of function's execution completing successfully
304306
305307
Parameters
306308
----------
307309
event: Dict[str, Any]
308310
Lambda event
311+
context: LambdaContext
312+
Lambda context
309313
result: dict
310314
The response from lambda handler
311315
"""
312316
response_data = json.dumps(result, cls=Encoder)
313317

314318
data_record = DataRecord(
315-
idempotency_key=self._get_hashed_idempotency_key(event),
319+
idempotency_key=self._get_hashed_idempotency_key(event, context),
316320
status=STATUS_CONSTANTS["COMPLETED"],
317321
expiry_timestamp=self._get_expiry_timestamp(),
318322
response_data=response_data,
@@ -326,17 +330,19 @@ def save_success(self, event: Dict[str, Any], result: dict) -> None:
326330

327331
self._save_to_cache(data_record)
328332

329-
def save_inprogress(self, event: Dict[str, Any]) -> None:
333+
def save_inprogress(self, event: Dict[str, Any], context: LambdaContext) -> None:
330334
"""
331335
Save record of function's execution being in progress
332336
333337
Parameters
334338
----------
335339
event: Dict[str, Any]
336340
Lambda event
341+
context: LambdaContext
342+
Lambda context
337343
"""
338344
data_record = DataRecord(
339-
idempotency_key=self._get_hashed_idempotency_key(event),
345+
idempotency_key=self._get_hashed_idempotency_key(event, context),
340346
status=STATUS_CONSTANTS["INPROGRESS"],
341347
expiry_timestamp=self._get_expiry_timestamp(),
342348
payload_hash=self._get_hashed_payload(event),
@@ -349,18 +355,20 @@ def save_inprogress(self, event: Dict[str, Any]) -> None:
349355

350356
self._put_record(data_record)
351357

352-
def delete_record(self, event: Dict[str, Any], exception: Exception):
358+
def delete_record(self, event: Dict[str, Any], context: LambdaContext, exception: Exception):
353359
"""
354360
Delete record from the persistence store
355361
356362
Parameters
357363
----------
358364
event: Dict[str, Any]
359365
Lambda event
366+
context: LambdaContext
367+
Lambda context
360368
exception
361369
The exception raised by the lambda handler
362370
"""
363-
data_record = DataRecord(idempotency_key=self._get_hashed_idempotency_key(event))
371+
data_record = DataRecord(idempotency_key=self._get_hashed_idempotency_key(event, context))
364372

365373
logger.debug(
366374
f"Lambda raised an exception ({type(exception).__name__}). Clearing in progress record in persistence "
@@ -370,14 +378,17 @@ def delete_record(self, event: Dict[str, Any], exception: Exception):
370378

371379
self._delete_from_cache(data_record.idempotency_key)
372380

373-
def get_record(self, event: Dict[str, Any]) -> DataRecord:
381+
def get_record(self, event: Dict[str, Any], context: LambdaContext) -> DataRecord:
374382
"""
375383
Calculate idempotency key for lambda_event, then retrieve item from persistence store using idempotency key
376384
and return it as a DataRecord instance.and return it as a DataRecord instance.
377385
378386
Parameters
379387
----------
380388
event: Dict[str, Any]
389+
Lambda event
390+
context: LambdaContext
391+
Lambda context
381392
382393
Returns
383394
-------
@@ -392,7 +403,7 @@ def get_record(self, event: Dict[str, Any]) -> DataRecord:
392403
Event payload doesn't match the stored record for the given idempotency key
393404
"""
394405

395-
idempotency_key = self._get_hashed_idempotency_key(event)
406+
idempotency_key = self._get_hashed_idempotency_key(event, context)
396407

397408
cached_record = self._retrieve_from_cache(idempotency_key=idempotency_key)
398409
if cached_record:

Diff for: tests/functional/idempotency/conftest.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import hashlib
33
import json
44
import os
5+
from collections import namedtuple
56
from decimal import Decimal
67
from unittest import mock
78

@@ -34,6 +35,18 @@ def lambda_apigw_event():
3435
return event
3536

3637

38+
@pytest.fixture
39+
def lambda_context():
40+
lambda_context = {
41+
"function_name": "test-func",
42+
"memory_limit_in_mb": 128,
43+
"invoked_function_arn": "arn:aws:lambda:eu-west-1:809313241234:function:test-func",
44+
"aws_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72",
45+
}
46+
47+
return namedtuple("LambdaContext", lambda_context.keys())(*lambda_context.values())
48+
49+
3750
@pytest.fixture
3851
def timestamp_future():
3952
return str(int((datetime.datetime.now() + datetime.timedelta(seconds=3600)).timestamp()))
@@ -132,18 +145,18 @@ def expected_params_put_item_with_validation(hashed_idempotency_key, hashed_vali
132145

133146

134147
@pytest.fixture
135-
def hashed_idempotency_key(lambda_apigw_event, default_jmespath):
148+
def hashed_idempotency_key(lambda_apigw_event, default_jmespath, lambda_context):
136149
compiled_jmespath = jmespath.compile(default_jmespath)
137150
data = compiled_jmespath.search(lambda_apigw_event)
138-
return hashlib.md5(json.dumps(data).encode()).hexdigest()
151+
return "test-func#" + hashlib.md5(json.dumps(data).encode()).hexdigest()
139152

140153

141154
@pytest.fixture
142155
def hashed_idempotency_key_with_envelope(lambda_apigw_event):
143156
event = unwrap_event_from_envelope(
144157
data=lambda_apigw_event, envelope=envelopes.API_GATEWAY_HTTP, jmespath_options={}
145158
)
146-
return hashlib.md5(json.dumps(event).encode()).hexdigest()
159+
return "test-func#" + hashlib.md5(json.dumps(event).encode()).hexdigest()
147160

148161

149162
@pytest.fixture

0 commit comments

Comments
 (0)