transactions multi-table MVP#688
Conversation
andrykonchin
left a comment
There was a problem hiding this comment.
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. |
| table_name: model_class.table_name, | ||
| } | ||
| } | ||
| end |
There was a problem hiding this comment.
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.
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.
@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.
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.
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.
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. |
| 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.
minor: Not the perfect name for a class.
There was a problem hiding this comment.
Any suggestions? This class wraps TransactWriteItems and someday maybe TransactGetItems as named by AWS.
There was a problem hiding this comment.
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.
It makes sense to test transaction rollback - non of changes should be persisted.
There was a problem hiding this comment.
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.
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.
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.
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.
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
left a comment
There was a problem hiding this comment.
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.
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.
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.
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.
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.
update with instance has different signature:
dynamoid/lib/dynamoid/persistence.rb
Lines 748 to 750 in 398f065
It's OK to skip this method for now.
There was a problem hiding this comment.
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.