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

CORE-1836 Optimistic locking with version number #24

Merged
merged 11 commits into from
Oct 29, 2021

Conversation

benrj
Copy link
Contributor

@benrj benrj commented Oct 20, 2021

Inspired by: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBMapper.OptimisticLocking.html

This adds optimistic locking to this library. In short, it optionally requires (after the first put/update) an item being mutated to include a version number that matches the current version number in the database. If it does not, it assumes it is trying to mutate a stale item and throws an error.

We won't support this for batch write operations, as the Dynamo APIs won't allow for use of conditional expressions in those operations. Question: Should we throw if a caller uses a batch write operation for a DataModel that has optimistic locking enabled? Or require them to specify the ignoreOptimisticLocking flag? Or just live and let live?

benrj added 6 commits October 19, 2021 16:42
This will set the stage for implementing optimistic locking via conditional
expressions.
This enforces the lock (except where ignored)
Extract and reduce duplication
Must cast data to avoid tripping the linter, otherwise, it'll complain
about expression of type 'string' can't be used to index type 'unknown'.
@benrj benrj requested review from ctdio and tywalch October 20, 2021 04:34
@benrj benrj requested a review from harlowk October 20, 2021 17:12
austin-rausch
austin-rausch previously approved these changes Oct 22, 2021
Copy link
Contributor

@austin-rausch austin-rausch left a comment

Choose a reason for hiding this comment

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

This is awesome, I was probably going to be needing something like this soon so thanks for doing this!

Only other thing I have to mention is really just to feed discussion around it.

The update call doesn't increment the version attribute like the put call does, the linked doc mentions updates incrementing the number for the transactionWrite's update, but doesn't explicitly say otherwise.

I wonder if this should be either default behavior or opt-in? E.G. incrementVersionNumberOnUpdate?: boolean

Curious what everyone else thinks.

Nevermind, I see the increment is in the update params. This is awesome, thanks!

src/index.ts Outdated
@@ -337,6 +454,9 @@ export default class DynamoDbDao<DataModel, KeySchema> {
key,
data,
...updateOptions,
optimisticLockVersionAttribute: this.optimisticLockingAttribute
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like your build is failing with a silly type error here since optimisticLockingAttribute?: keyof NumberPropertiesInType<T> is technically not assignable directly to undefined, it's just optional. You could either union that or do this fancy / semi hard to read conditional object spread syntax:

...(this.optimisticLockingAttribute
      ? {
          optimisticLockingAttribute:
            this.optimisticLockingAttribute.toString(),
        }
      : {}),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! As best I can tell, TS is happy with this line, but was displeased with line 103 in TestContext.ts. I did try your suggestion there but got the same complaint.

I ended up casting it like so:

const dao = new DynamoDbDao<DataModel, KeySchema>({
  tableName,
  documentClient,
  optimisticLockingAttribute: useOptimisticLocking ? 'version' : undefined,
} as DynamoDbDaoInput<DataModel>);

That said, I would like to know why it complains that Type 'string' is not assignable to type 'undefined' without the cast.

if (this.optimisticLockingAttribute) {
// Must cast data to avoid tripping the linter, otherwise, it'll complain
// about expression of type 'string' can't be used to index type 'unknown'
const dataAsMap = data as DataModelAsMap;
Copy link
Contributor

@austin-rausch austin-rausch Oct 22, 2021

Choose a reason for hiding this comment

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

I see why this is necessary, but I wonder if its possible to get a bit fancier with the NumberPropertiesInType and the constraint for the DataModel generic.

Maybe make the DataModel type explicitly extend a string record Record<string, any> or if that index type doesn't work because of the generic of Record {[key: string]: any}

Then you could get fancier with the NumberPropertiesInType and add a second ternary to make sure that the key is a string type.

E.G.

type NumberProp<T extends Record<string, any>> = Pick<
  T,
   {
     [K in keyof T]: K extends string ?
       T[k] extends number ? K
       : never
      : never;
    }[keyof T]
>;

That being said, all of that would become very hard to read & understand so this solution is probably better, we still have the guarantees on the constructor args that its a number attribute, just a slight risk with these casts that it could be used incorrectly in internal methods, but your tests are extremely thorough so thats unlikely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a nifty idea.. although I think I might need to consider that a potential breaking change since it limits the DataModel generic.

Apart from that, though, when I tried that, I ran into this => microsoft/TypeScript#31661. 🤮

Appeasing (coercing!) the linter
if (versionAttributeValue) {
attributeNames = {
...attributeNames,
[`#${versionAttribute}`]: versionAttribute,
Copy link

Choose a reason for hiding this comment

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

Could the attribute name/value properties be post-fixed with a unique value (like _lock)? It would prevent accidentally overwriting a user's value if there ever was an overlap given how freeform things can be with the DAO

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about that, but since I'm letting the package caller tell me which attribute they want to store the version in (and enforcing it as numeric property of DataModel), I figure I'd let the callers deal with the potential consequences of choosing a version field.

I.e. if there is an overlap - the caller explicitly made that decision.

benrj added 2 commits October 26, 2021 13:15
We'll consider this a non-breaking change, since optimistic locking is
completely optional
@@ -137,6 +137,32 @@ const { total } = await myDocumentDao.decr(
);
```

**Optimistic Locking with Version Numbers**
Copy link

Choose a reason for hiding this comment

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

Would be helpful to add a snippet that shows how to enable optimistic locking for the dao here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea - I've added that (along with a note in the changelog).

ctdio
ctdio previously approved these changes Oct 27, 2021
Copy link

@ctdio ctdio left a comment

Choose a reason for hiding this comment

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

Nice job with this! This is gonna be super helpful

@benrj benrj merged commit d7561dd into main Oct 29, 2021
@benrj benrj deleted the core-1836-optimistic-locking branch October 29, 2021 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants