Skip to content
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

Merged
merged 51 commits into from
Nov 11, 2022

Conversation

jeffrey-baker-vg
Copy link
Contributor

@jeffrey-baker-vg jeffrey-baker-vg commented Oct 6, 2022

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:

  1. The ability to create and update Idempotency records in all possible statuses
  2. Hashing the input to the function and using that hash to create the idempotency key
  3. Defaulting the persistence layer to a DynamoDB table and creating the concrete class to make updates to this table

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

  • My changes meet the tenets criteria
  • I have performed a self-review of my own code
  • I have commented my code where necessary, particularly in areas that should be flagged with a TODO, or hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have made corresponding changes to the examples
  • My changes generate no new warnings
  • The code coverage hasn't decreased
  • I have added tests that prove my change is effective and works
  • New and existing unit tests pass locally and in Github Actions
  • Any dependent changes have been merged and published in downstream module
  • The PR title follows the conventional commit semantics

Breaking change checklist

  • I have documented the migration process
  • I have added, implemented necessary warnings (if it can live side by side)

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@dreamorosi
Copy link
Contributor

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.

@jeffrey-baker-vg
Copy link
Contributor Author

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.

@ijemmy ijemmy added the on-hold This item is on-hold and will be revisited in the future label Oct 11, 2022
@ijemmy
Copy link
Contributor

ijemmy commented Oct 11, 2022

@dreamorosi

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 on-hold to avoid confusion.

vgphoenixcampos and others added 26 commits October 27, 2022 16:45
Copy link
Contributor

@ijemmy ijemmy left a 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 constructors 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 */
Copy link
Contributor

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?

Comment on lines 12 to 15
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') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things:

  1. Let's keep the variable naming convention in TypeScript way ,camelCase rather than snakeCase. For the value inserted in DynamoDB (e.g. in_progress_expiry_attr, let's stick with what Python team uses for consistency)
  2. Can we make this one line per variable for readability?
  3. 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.

Python's logger:
image

TypeScript Logger
image

image

Copy link
Contributor

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.

Comment on lines 28 to 32
const output: GetCommandOutput = await table.get(
{
TableName: this.tableName, Key: { [this.key_attr]: idempotencyKey }
}
);
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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() };
Copy link
Contributor

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 });
Copy link
Contributor

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: { 
      //...
   },
   //...
 });

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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

Copy link
Contributor

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.

@ijemmy ijemmy requested a review from dreamorosi October 31, 2022 18:40
@ijemmy ijemmy removed the on-hold This item is on-hold and will be revisited in the future label Oct 31, 2022
Copy link
Contributor

@dreamorosi dreamorosi left a 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):

``` ~/Codes/aws-lambda-powertools-typescript/packages/idempotency pr/jeffrey-baker-vg/1110 14s ❯ npm i

@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>

package.json Outdated Show resolved Hide resolved
Comment on lines 12 to 15
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') {
Copy link
Contributor

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.

Comment on lines 28 to 32
const output: GetCommandOutput = await table.get(
{
TableName: this.tableName, Key: { [this.key_attr]: idempotencyKey }
}
);
Copy link
Contributor

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 });
Copy link
Contributor

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.

packages/idempotency/src/persistence/IdempotencyRecord.ts Outdated Show resolved Hide resolved
Comment on lines 72 to 73
if (!this._table)
this._table = DynamoDBDocument.from(new DynamoDB({}), { marshallOptions: { removeUndefinedValues: true } });
Copy link
Contributor

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.

packages/idempotency/src/persistence/IdempotencyRecord.ts Outdated Show resolved Hide resolved
packages/idempotency/src/persistence/PersistenceLayer.ts Outdated Show resolved Hide resolved
@@ -0,0 +1,2 @@
// Reserved variables
process.env.AWS_REGION = 'us-east-1';
Copy link
Contributor

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

Comment on lines +5 to +8
saveInProgress(data: unknown): Promise<void>
saveSuccess(data: unknown, result: unknown): Promise<void>
deleteRecord(data: unknown): Promise<void>
getRecord(data: unknown): Promise<IdempotencyRecord>
Copy link
Contributor

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.

Copy link
Contributor

@ijemmy ijemmy left a 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. :)

Copy link
Contributor

@dreamorosi dreamorosi left a 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!

@ijemmy ijemmy changed the title Feat: Persistence Layer feat(idempotency): Add persistence layer and DynamoDB implementation Nov 11, 2022
@dreamorosi dreamorosi merged commit 0a6676a into aws-powertools:main Nov 11, 2022
@ijemmy ijemmy added the idempotency This item relates to the Idempotency Utility label Jan 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
idempotency This item relates to the Idempotency Utility
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants