-
-
Notifications
You must be signed in to change notification settings - Fork 194
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
transactions multi-table MVP #688
Conversation
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've left some comments. It isn't a call to action to make any change but is a way to get you more familiar with the project structure and its conventions.
@andrykonchin I made some updates to the transaction API and now there is no longer a mess of hashes being passed around. Your feedback would be appreciated. Some minor cleanup and tests are still needed. |
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.
Looks pretty good!
lib/dynamoid/transaction/upsert.rb
Outdated
table_name: model_class.table_name, | ||
} | ||
} | ||
end |
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.
Class-level methods like create
/update
/upsert
accepts keys and attributes as separate parameters:
User.upsert('1', age: 26)
This way we can find the proper item in DynamoDB even when user updates sort key for instance (can he change a partition key? probably he can)
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.
Yes! I changed update and delete to take an id which can be a hash_key or an [hash_key, range_key] like is used by find(). I don't think id is needed for create and upsert since those are both putting complete records. Should we include id anyways for those?
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.
@andrykonchin I assumed that the partition key could be changed but that is not true. A record must be deleted and recreated if you wish to change the partition key. How would you feel about simplifying the APIs for update and upsert?
Instead of:
txn.update!(User, '1', {name: 'bob'} )
it would be:
txn.update!(User, {id: '1', name: 'bob'} )
When there's a range key:
txn.update!(User, ['1', 26], {name: 'bob'} )
would be:
txn.update!(User, {id: '1', age: 26, name: 'bob'} )
This makes the API similar to create which I like but perhaps it's too inconsistent with the other update and upserts.
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 makes the API similar to create which I like but perhaps it's too inconsistent with the other update and upserts.
It seems to me it will not work for updating a range key attribute. Its current value should be used to find an item and a new value to set.
Current approach for transaction methods is to be as close to the existing methods as possible. If there are reasons to change method signature - it makes sense to change both existing method and a transactional one (but it would be a breaking change so it would require a major release).
I don't think id is needed for create and upsert since those are both putting complete records. Should we include id anyways for those?
So create
shouldn't accept id as a separate parameter, but upsert
should.
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.
No part of a primary key can be updated. The record must be deleted and re-created to change the key. If it's a composite key this includes the range key which cannot be changed.
I see your point about wanting to be consistent with the non-transaction methods however not allowing a separate key to be specified may be a better direction to go in. This will discourage people from trying to update primary keys among other things. I like the shorter method signature and that all the transaction method signatures are more similar as well.
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.
No part of a primary key can be updated. The record must be deleted and re-created to change the key. If it's a composite key this includes the range key which cannot be changed.
Indeed, exception is raised:
One or more parameter values were invalid: Cannot update attribute age. This attribute is part
of the key (Aws::DynamoDB::Errors::ValidationException)
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## master #688 +/- ##
==========================================
+ Coverage 90.32% 90.89% +0.56%
==========================================
Files 62 71 +9
Lines 3154 3414 +260
==========================================
+ Hits 2849 3103 +254
- Misses 305 311 +6 ☔ View full report in Codecov by Sentry. |
@andrykonchin Another round of feedback would be appreciated. I believe all issues were covered although adding block support for updates is something that could still be done. This might be a cleaner place to add add() expressions. I'd like to get to a v1 candidate at some point with unit tests included. |
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.
Looks good!
@@ -8,6 +8,7 @@ | |||
require_relative 'aws_sdk_v3/item_updater' | |||
require_relative 'aws_sdk_v3/table' | |||
require_relative 'aws_sdk_v3/until_past_table_status' | |||
require_relative 'aws_sdk_v3/transact' |
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.
minor: Not the perfect name for a class.
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.
Any suggestions? This class wraps TransactWriteItems and someday maybe TransactGetItems as named by AWS.
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.
TBH I would move transact_write_items
into the main plugin class. It's small and simple enough (what was a reason to move some methods into separate classes)
@andrykonchin Thanks for the feedback. There are now proper delete and destroys. Upsert is working like an upsert as well. |
Looks good so we can move on and and add specs. |
@andrykonchin There's now a collection of specs for the transaction writes. Can you run them on github or give me permission to run them? What else is needed to get v1 released? Thanks! |
The new specs look good, but I would prefer to have more granular test cases that check only one aspect of a method behaviour (ideally only one check/assert per case) |
Yeah, I usually put 4 or 5 syntax checks in each case. I'll split those out. |
expect(obj1_found.name).to eql("one") | ||
expect(obj2_found.name).to eql("two") | ||
expect(obj3_found.name).to eql("three") | ||
end |
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.
It makes sense to test transaction rollback - non of changes should be persisted.
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 makes sense to check a case when some model isn't valid - it shouldn't rollback transaction.
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.
Validation tests now check there is no rollback with non-bang saves and that there is a rollback with bang! saves.
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.
Yeah, I see. So there are still cases to check that a transaction is rolled back when some of operations fails on DynamoDB side, e.g. deleting/updating not existing item, creation of item which primary key already exists.
So it makes sense to add a case for each transactional method (create/update/delete etc) to ensure that transaction is atomic.
@andrykonchin Thanks for the specs review. What are your thoughts on this one? |
A failing spec is fixed on master. |
@andrykonchin update() and upsert() no longer allow a key to be specified separate from the attributes. A key must always be included in the model or in the attributes. This simplifies the method signatures and makes them consistent with the other transaction actions. I believe I covered all your feedback on the specs so another round of review would be good. Thanks!!! |
expect(obj1_found.name).to eql("oneone") | ||
expect(obj2_found.name).to eql("two") | ||
end | ||
end |
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 believe validation should be tested here as well
expect(obj1_found.name).to eql("one") | ||
expect(obj2_found.name).to eql("two") | ||
expect(obj3_found.name).to eql("three") | ||
end |
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.
Yeah, I see. So there are still cases to check that a transaction is rolled back when some of operations fails on DynamoDB side, e.g. deleting/updating not existing item, creation of item which primary key already exists.
So it makes sense to add a case for each transactional method (create/update/delete etc) to ensure that transaction is atomic.
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.
Specs look much better. Thank you!
include_context 'transaction_write' | ||
|
||
# a 'put' is a create that overwrites existing records if present | ||
context 'puts' do |
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.
The create
method doesn't overwrite existing document with the same primary key and raises Dynamoid::Errors::RecordNotUnique
instead.
https://github.com/Dynamoid/dynamoid/blob/master/lib/dynamoid/persistence/save.rb#L50
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'm allowing create() to behave like a "put" when skip_existence_check: true
is used. This does an overwrite if a previous record exists. The relatively short put_spec tests this. Maybe renaming to allow_overwrite: true
would be better? Or add a put()?
Currently in a transaction when trying to create a record when it already exists raises Aws::DynamoDB::Errors::TransactionCanceledException.
expect(klass_with_validation.exists?(obj1.id)).to be_falsey | ||
expect(obj2.id).to be_present | ||
expect(klass_with_validation.exists?(obj2.id)).to be_falsey | ||
end |
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 check rolling back of the whole transaction in a separate test case with corresponding title. Here and in specs for other methods.
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.
Added explicit rollback specs.
obj1 = klass.create!(name: "one") | ||
obj1.name = "oneone" | ||
Dynamoid::TransactionWrite.execute do |txn| | ||
txn.update! obj1 |
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.
update
with instance has different signature:
dynamoid/lib/dynamoid/persistence.rb
Lines 748 to 750 in 398f065
# user.update(unless_exists: [:age]) do |t| | |
# t.set(age: 18) | |
# end |
It's OK to skip this method for now.
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.
OK, skipping support for blocks in update() for now. I would like to support increment eventually.
@andrykonchin More tests were added for rollbacks and callbacks. Instance usage tests for upsert were removed. |
I removed the named parameter for options. This simplifies the brackets for expected usage e.g.:
instead of:
|
@andrykonchin I believe all feedback has been addressed and further review would be appreciated. |
@andrykonchin Anything blocking this? Thanks. |
Could you please fix Rubocop issues (related to the changes only - there are some existing issues on master - I will address them myself) and squash all the working commits into one or more atomic ones? |
I want to release a new version without transactions now and release transactions in the next version. The last release was a year ago and I was already asked for new version. |
@andrykonchin I squashed the commits and I believe Rubocops will pass. Anything else I need to do? There are Codeclimate complexity issues. |
Merged! 🎉 Great work! |
That's great news! Thanks for the support. |
This is a first attempt at dynamodb transactions that can span tables. Create, update, upsert and delete are supported. See README_transact.md for examples of the proposed API.
Tests have not yet been implemented.