-
Notifications
You must be signed in to change notification settings - Fork 146
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): Add persistence layer and DynamoDB implementation #1110
Conversation
Hi team, thank you for the PR! Before we can start reviewing it could you please take some time to rebase the PR, address the merge conflicts, and also fill in the PR details? It would be really helpful given the amount of new code to have some insights. Really appreciate your time and effort on this. |
Absolutely! As you can probably tell, this is very incomplete. This pull request was actually intended for my forked repo, with the idea being that I would just open a pull request and add to it so the team could stay in sync as we make changes to multiple files. I must have been multi-tasking when I opened the PR, since it was definitely not intended to go here yet. |
Just to clarify, @jeffrey-baker-vg will rebase and add more code regarding persistence layer later. For now, please ignore this issue. I labelled this |
…, and json response
6b962ae
to
0ec8301
Compare
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.
This looks very good now. I really appreciate the comprehensive test cases. Given the number of users, I believe this will save us from regression bugs in the future.
The biggest comments are the constructor
s of DynamoDBPersistenceLayer
and IdempotencyRecord
. I think we should make them more TypeScript
style and be consistent with other TypeScript utilities. While we base our implementation on Python one, anything that is not exposed to public can be changed as we see fit.
Apart from that, I have some questions and small nit picking on test cases' names. I put a "suggestion" for quick fix, you can just accept and put the commit in. (We'll squash merge in the end.)
Another change request
Can you remove changes in files under layer-publisher
folder? (12'
and package-lock.json
)
@@ -1,18 +1,81 @@ | |||
/* eslint-disable @typescript-eslint/no-empty-function */ |
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.
Can we remove this now?
public constructor(private tableName: string, private key_attr: string = 'id', | ||
private status_attr: string = 'status', private expiry_attr: string = 'expiration', | ||
private in_progress_expiry_attr: string = 'in_progress_expiry_attr', | ||
private data_attr: string = 'data') { |
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.
A few things:
- Let's keep the variable naming convention in TypeScript way ,
camelCase
rather thansnakeCase
. For the value inserted in DynamoDB (e.g.in_progress_expiry_attr
, let's stick with what Python team uses for consistency) - Can we make this one line per variable for readability?
- Can we pass an object
DynamoDBPersistenceLayerOption
instead? It's more common for TypeScript to use an argument object rather than a long list of parameter. This also eliminate the need of "order" and easier to use. Logger also uses the same convention.
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.
+1 on this, let's make this an object with its dedicated type like in the other utilities.
const output: GetCommandOutput = await table.get( | ||
{ | ||
TableName: this.tableName, Key: { [this.key_attr]: idempotencyKey } | ||
} | ||
); |
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.
Do we need a strong consistency read here?
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.
Also I'm curious about the naming convention with the _
prefix.
If it's used to specify the access pattern let's use the private
, protected
, public
, etc. 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.
Removing the _
creates a naming conflict between this and its parent class. We could change the name from getRecord to something else to solve this. In the Python library the parent also defines a more a generic getRecord and relies on the implementation of the abstract class to define a more specific _getRecord that does any logic specific to the concrete persistence layer.
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.
Understood, thanks.
protected async _putRecord(_record: IdempotencyRecord): Promise<void> { | ||
const table: DynamoDBDocument = this.getTable(); | ||
|
||
const item = { [this.key_attr]: _record.idempotencyKey, [this.expiry_attr]: _record.expiryTimestamp, [this.status_attr]: _record.getStatus() }; |
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.
Can we break this into multiple lines for readability?
const notInProgress = 'NOT #status = :inprogress'; | ||
const conditionalExpression = `${idempotencyKeyDoesNotExist} OR ${idempotencyKeyExpired} OR ${notInProgress}`; | ||
try { | ||
await table.put({ TableName: this.tableName, Item: item, ExpressionAttributeNames: { '#id': this.key_attr, '#expiry': this.expiry_attr, '#status': this.status_attr }, ExpressionAttributeValues: { ':now': Date.now() / 1000, ':inprogress': IdempotencyRecordStatus.INPROGRESS }, ConditionExpression: conditionalExpression }); |
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.
Can we break this into one line per field?
await table.put({
TableName: this.tableName,
Item: item,
ExpressionAttributeNames: {
//...
},
//...
});
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.
I would consider setting up linting with the same config as the rest of the project and then run npm run lint-fix
so that all these are taken care of.
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.
Do you have any specific recommendations for configurating the linting for this package like the rest of the project? I tried adding this package to the lerna.json so it would run the same lint-fix command on the idempotency package, but it did not fix the issue. Running lint fix on the project itself also doesn't do anything different.
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.
@vgphoenixcampos Embarrassingly, we actually don't have this. I use the default one on VSCode but it doesn't break objects into multiple lines.
@dreamorosi How do you this? I'm thinking about using prettier as part of lint-fix
target or commit hook. It has plugins for both VSCode and IntelliJ.
Then we will have a massive PR with only formatting :D
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.
Yea, let's park this topic for now and prioritise the logic, you can disregard my comment for now.
I like prettier or similar but I'd like to find a way to do it without messing entirely the history. I know there might be ways but it requires some investigation. For now let's just work with eslint
with the config that is already in the project.
packages/idempotency/tests/unit/persistence/PersistenceLayer.test.ts
Outdated
Show resolved
Hide resolved
packages/idempotency/tests/unit/persistence/PersistenceLayer.test.ts
Outdated
Show resolved
Hide resolved
packages/idempotency/tests/unit/persistence/PersistenceLayer.test.ts
Outdated
Show resolved
Hide resolved
packages/idempotency/tests/unit/persistence/PersistenceLayer.test.ts
Outdated
Show resolved
Hide resolved
packages/idempotency/tests/unit/persistence/PersistenceLayer.test.ts
Outdated
Show resolved
Hide resolved
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.
Thanks a lot folks for all the work on this PR, this is looking better and better every time I check it.
I have left a number of comments on the code which are mostly in line with the review that Jemmy has already left you. None of the comment is major in terms of impact and most of the changes I'd like to see are related to coding style/structure rather than logic - which is good and expected.
Finally, I also tried to run the tests locally after checking out your branch but was not able to. I get the following errors after running npm I
(expand to see):
@aws-lambda-powertools/[email protected] prepare
npm run build
@aws-lambda-powertools/[email protected] build
tsc
node_modules/@types/sinon/index.d.ts:1447:53 - error TS2694: Namespace '"/Users/aamorosi/Codes/aws-lambda-powertools-typescript/packages/idempotency/node_modules/@sinonjs/fake-timers/types/fake-timers-src"' has no exported member 'FakeTimerInstallOpts'.
1447 useFakeTimers: boolean | Partial<FakeTimers.FakeTimerInstallOpts>;
~~~~~~~~~~~~~~~~~~~~
node_modules/@types/sinon/index.d.ts:1569:67 - error TS2694: Namespace '"/Users/aamorosi/Codes/aws-lambda-powertools-typescript/packages/idempotency/node_modules/@sinonjs/fake-timers/types/fake-timers-src"' has no exported member 'FakeTimerInstallOpts'.
1569 useFakeTimers(config?: number | Date | Partial<FakeTimers.FakeTimerInstallOpts>): SinonFakeTimers;
~~~~~~~~~~~~~~~~~~~~
src/persistence/DynamoDbPersistenceLayer.ts:3:52 - error TS2307: Cannot find module '@aws-sdk/lib-dynamodb' or its corresponding type declarations.
3 import { DynamoDBDocument, GetCommandOutput } from '@aws-sdk/lib-dynamodb';
~~~~~~~~~~~~~~~~~~~~~~~
Found 3 errors in 2 files.
Errors Files
2 node_modules/@types/sinon/index.d.ts:1447
1 src/persistence/DynamoDbPersistenceLayer.ts:3
npm ERR! code 2
npm ERR! path /Users/aamorosi/Codes/aws-lambda-powertools-typescript/packages/idempotency
npm ERR! command failed
npm ERR! command sh -c npm run build
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/aamorosi/.npm/_logs/2022-11-02T14_23_18_191Z-debug-0.log
</details>
public constructor(private tableName: string, private key_attr: string = 'id', | ||
private status_attr: string = 'status', private expiry_attr: string = 'expiration', | ||
private in_progress_expiry_attr: string = 'in_progress_expiry_attr', | ||
private data_attr: string = 'data') { |
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.
+1 on this, let's make this an object with its dedicated type like in the other utilities.
const output: GetCommandOutput = await table.get( | ||
{ | ||
TableName: this.tableName, Key: { [this.key_attr]: idempotencyKey } | ||
} | ||
); |
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.
Also I'm curious about the naming convention with the _
prefix.
If it's used to specify the access pattern let's use the private
, protected
, public
, etc. instead.
const notInProgress = 'NOT #status = :inprogress'; | ||
const conditionalExpression = `${idempotencyKeyDoesNotExist} OR ${idempotencyKeyExpired} OR ${notInProgress}`; | ||
try { | ||
await table.put({ TableName: this.tableName, Item: item, ExpressionAttributeNames: { '#id': this.key_attr, '#expiry': this.expiry_attr, '#status': this.status_attr }, ExpressionAttributeValues: { ':now': Date.now() / 1000, ':inprogress': IdempotencyRecordStatus.INPROGRESS }, ConditionExpression: conditionalExpression }); |
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.
I would consider setting up linting with the same config as the rest of the project and then run npm run lint-fix
so that all these are taken care of.
if (!this._table) | ||
this._table = DynamoDBDocument.from(new DynamoDB({}), { marshallOptions: { removeUndefinedValues: true } }); |
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.
On principle I like the lazy init as well but I'm not sure the naming/location is correct.
Looking at how boto3 has a Table high-level client the method name of getTable
& logic makes sense. However in the JS SDK this is a generic DynamoDB client so I would expect to find this at this.client
/this.provider
instead of this._table
.
To further this, when used (few lines above) you still have to always pass a TableName
property anyway. So essentially I would rename this method to getClient
and change usages to things like client.update
rather than table.update
.
Aside from the naming/location however, I think we should consider having the client as a class prop. This would give customers the chance to pass their own client with custom configs (i.e. different region or credentials) as well as apply tracing to it.
@@ -0,0 +1,2 @@ | |||
// Reserved variables | |||
process.env.AWS_REGION = 'us-east-1'; |
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 that this file is still being used? In the diff above I see that the code that calls it was removed
saveInProgress(data: unknown): Promise<void> | ||
saveSuccess(data: unknown, result: unknown): Promise<void> | ||
deleteRecord(data: unknown): Promise<void> | ||
getRecord(data: unknown): Promise<IdempotencyRecord> |
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.
- on this, let's make an effort to type as much as we can & use generics as needed.
unknown
should be the last options.
Co-authored-by: ijemmy <[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.
Thank you very much. :)
packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts
Show resolved
Hide resolved
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.
Thank you for the PR and for addressing our comments.
We really appreciate the contribution to this project!
Description of your changes
This PR includes the creation the the persistence layer code in the Idempotency package necessary to provide the the most basic idempotency functionality. This includes:
This PR is the first step toward adding basic idempotency functionality. Once the code is merged, we plan to create the makeFunctionIdempotent function wrapper, which will use this persistence layer to implement the minimum code required to make a function idempotent.
How to verify this change
Related issues, RFCs
Issue number: 447
PR status
Is this ready for review?: YES
Is it a breaking change?: NO
Checklist
Breaking change checklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.