-
Notifications
You must be signed in to change notification settings - Fork 153
Bug: IdempotencyItemAlreadyExistsError
loses referential identity when IdempotencyConfig
is imported via external pkg
#2517
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
Comments
Thanks for opening your first issue here! We'll come back to you as soon as we can. |
Hi @mrerichoffman - thank you for opening the issue & providing the example. I'm going to try to reproduce the issue on my end and will be back to you! |
Hi, I have been looking at reproducing this and I was not able to do so. Running your exact code on a function backed by a DynamoDB table using composite primary key I was able to execute the function more than once using the same payload and observe that subsequent requests are being considered idempotent (see screenshot below): In the sentence above I specifically called out that the DynamoDB table uses composite primary key, meaning the configuration provided to the This is a non standard setup, and if you have deployed a Table using the configuration suggested in our docs, then the key schema would be with a partition key named With this in mind, is there any other info that you think could be relevant in helping me reproduce the error consistently? Alternatively, I also want to call out that this error is expected when two concurrent identical in-flight requests are being handled. As the error states, if a request arrives while a previously received identical one is still in progress, any subsequent one is rejected. This flow is detailed in this diagram in the docs and is also explained here. If your first request failed to process and the idempotency record remained in an "in progress" state, subsequent requests made within what we call "in progress expiration" time are also rejected. This time is set to the remaining time in milliseconds before the invocation will timeout. In other words, if a request starts being processed while the function has 100 seconds remaining, the corresponding idempotency record will be placed as "in progress" with an This locking mechanism is necessary due to the distributed nature of Lambda, since subsequent identical requests have no way of knowing whether the first original request is still being processed (i.e. long running process) or has failed/timed out. After that After that, if a new identical request comes it will be accepted and processed accordingly. |
Sorry, I must have tested the provided sample code before I ripped out additional code to simplify testing. As it turns out the bug is reproducable when getDefaultIdempotencyConfigs is externalized into a separate npm package which is bundled to commonjs. To fix I removed the idempotency utility from the npm package and exported json configurations instead. |
Thanks for letting me know. Just to make sure I understand, could you share an example of how to reproduce the issue? I'd like to see if there's anything that we can either improve in the code or docs to avoid others experiencing the same problem. Thanks! |
Sure, I externalized the idempotency configs into an example npm package that can be installed from the attached tarball. Updated lambda handler: import { makeHandlerIdempotent } from "@aws-lambda-powertools/idempotency/middleware";
import middy from "@middy/core";
import { getDefaultIdempotencyConfigs } from "example-config-package";
export interface ProcessResult {
processResult: "processed" | "rejected";
}
const { persistenceStore, idempotencyConfig } = getDefaultIdempotencyConfigs();
const lambdaHandler = async (event: Record<"string", unknown>): Promise<ProcessResult> => {
console.info(`event`, { event });
const processResult: ProcessResult = {
processResult: "processed"
};
return processResult;
};
export const testingPowertool = middy(lambdaHandler).use(
makeHandlerIdempotent({
persistenceStore,
config: idempotencyConfig
})
); npm package example-config-package referenced in import { IdempotencyConfig } from "@aws-lambda-powertools/idempotency";
import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/dynamodb";
export const getDefaultIdempotencyConfigs = (eventKeyJmesPath = '["correlationId"]') => {
const persistenceStore = new DynamoDBPersistenceLayer({
tableName: process.env.IDEMPOTENCY_TABLE_NAME || "IdempotencyTable",
sortKeyAttr: "sortKey"
});
const idempotencyConfig = new IdempotencyConfig({
throwOnNoIdempotencyKey: true,
eventKeyJmesPath
});
return {
persistenceStore,
idempotencyConfig
};
}; Place the tgz packed library in the root of the project. Reference the tzg file in package.json dependencies like this - |
Hi, I have been looking into this and I am able to reproduce the issue when I use the tarball you provided but not when I bundle/build the same utility on my end. When deploying my function using your Despite this, I haven't been able to pinpoint the issue yet and due to the fact that it appears only under these specific conditions I am not sure how to troubleshoot it properly. I'll still give it a try and get back to you. |
I'm back from PTO but this week I will be attending the AWS Summit in Madrid (Spain). I'll start looking at this as soon as I can but just to manage expectations - I might need a few more days. |
Hi @mrerichoffman, thank you for your patience. I have been looking at this on & off for the past couple days, built an isolated version of the config package, and then finally resorted to debug the The error we were both observing was consistently happening in subsequent invocations - aka any request that should've been considered idempotent. This led me to narrow down the root cause to this After stepping through the built code, I noticed that the error being caught in that block was failing the To explain why this With the current setup, the "import tree" of the dependency looks something like this:
Due to this, once bundled, some of the objects from the Idempotency utility lose their referential identity. Specifically, observing the final bundle we can see that the errors from here, as well as other objects are present twice in the bundle. We know that this is not a desirable outcome because we have authored both the This is good overall, even though in this specific instance it leads to some extra code being imported. There are ways to go around this, but they require using As a proof of this, I also marked the In this case In terms of solutions, as I mentioned there are For example, I'm inclined to update the error classes of the utility to have a more specific /**
* Item attempting to be inserted into persistence store already exists and is not expired
*/
class IdempotencyItemAlreadyExistsError extends Error {
public existingRecord?: IdempotencyRecord;
public constructor(message?: string, existingRecord?: IdempotencyRecord) {
super(message);
+ this.name = 'IdempotencyItemAlreadyExistsError';
this.existingRecord = existingRecord;
}
} so that we could then refactor the identity check within the try {
await this.#persistenceStore.saveInProgress(
this.#functionPayloadToBeHashed,
this.#idempotencyConfig.lambdaContext?.getRemainingTimeInMillis()
);
return returnValue;
} catch (e) {
- if (e instanceof IdempotencyItemAlreadyExistsError) {
+ if (e.name === 'IdempotencyItemAlreadyExistsError') { With this change, even though we would still not preventing I have tried this change with a custom build of the Idempotency utility and I was able to confirm that it solves the issue with your setup. I will work on a PR tomorrow and I'm positive that we should be able to include it in the next release. |
IdempotencyItemAlreadyExistsError
loses referential identity when IdempotencyConfig
is imported via external pkg
As a side note to the above, another way to avoid duplicating some of the objects and guide As of now, looking at the tarball you have provided you are authoring the package using In the By changing the - import { makeHandlerIdempotent } from "@aws-lambda-powertools/idempotency/middleware";
+ const {
+ makeHandlerIdempotent,
+ } = require('@aws-lambda-powertools/idempotency/middleware');
import middy from "@middy/core";
import { getDefaultIdempotencyConfigs } from "example-config-package";
export interface ProcessResult {
processResult: "processed" | "rejected";
}
const { persistenceStore, idempotencyConfig } = getDefaultIdempotencyConfigs();
const lambdaHandler = async (event: Record<"string", unknown>): Promise<ProcessResult> => {
console.info(`event`, { event });
const processResult: ProcessResult = {
processResult: "processed"
};
return processResult;
};
export const testingPowertool = middy(lambdaHandler).use(
makeHandlerIdempotent({
persistenceStore,
config: idempotencyConfig
})
); as well as changing the import in the - import { IdempotencyConfig } from "@aws-lambda-powertools/idempotency";
+ import { IdempotencyConfig } from "@aws-lambda-powertools/idempotency/config";
import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/dynamodb";
export const getDefaultIdempotencyConfigs = (eventKeyJmesPath = '["correlationId"]') => {
const persistenceStore = new DynamoDBPersistenceLayer({
tableName: process.env.IDEMPOTENCY_TABLE_NAME || "IdempotencyTable",
sortKeyAttr: "sortKey"
});
const idempotencyConfig = new IdempotencyConfig({
throwOnNoIdempotencyKey: true,
eventKeyJmesPath
});
return {
persistenceStore,
idempotencyConfig
};
}; the bundled output now includes only one copy of the error classes, which means the While I haven't tested this, I suspect that changing the build script of the Despite this, which is ultimately a case of dual package hazard, I'm still going to make the change to use branded errors (aka using |
This issue is now closed. Please be mindful that future comments are hard for our team to see. If you need more assistance, please either tag a team member or open a new issue that references this one. If you wish to keep having a conversation with other community members under this issue feel free to do so. |
This is now released under v2.4.0 version! |
Expected Behavior
When using the Idempotency utility with ddb persistence, the utility should return the result of a previous operation from cache provided that the request is handled by the same execution environment.
Current Behavior
When using ddb persistence the utility doesn't return the correct value from the cache, but instead causes a runtime error. If I revert from v2.1.0 to v1.17.0 the bug goes away.
Code snippet
Steps to Reproduce
Then run the function with the same event payload, { "correlationId": "abc-123-def-456" }, twice and observe the error (shown below) being thrown at the second execution.
Possible Solution
No response
Powertools for AWS Lambda (TypeScript) version
latest
AWS Lambda function runtime
20.x
Packaging format used
npm
Execution logs
The text was updated successfully, but these errors were encountered: