├── .github
└── PULL_REQUEST_TEMPLATE.md
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DESIGN.md
├── LICENSE.txt
├── META-INF
└── MANIFEST.MF
├── NOTICE.txt
├── README.md
├── build.properties
├── examples
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── com
│ │ └── amazonaws
│ │ └── services
│ │ └── dynamodbv2
│ │ └── transactions
│ │ └── examples
│ │ └── TransactionExamples.java
│ └── resources
│ └── com
│ └── amazonaws
│ └── services
│ └── dynamodbv2
│ └── transactions
│ └── examples
│ └── AwsCredentials.properties
├── integration
├── pom.xml
└── src
│ └── test
│ ├── java
│ └── com
│ │ └── amazonaws
│ │ └── services
│ │ └── dynamodbv2
│ │ └── transactions
│ │ ├── FailingAmazonDynamoDBClient.java
│ │ ├── IntegrationTest.java
│ │ ├── MapperTransactionsIntegrationTest.java
│ │ ├── TransactionManagerDBFacadeIntegrationTest.java
│ │ └── TransactionsIntegrationTest.java
│ └── resources
│ └── com
│ └── amazonaws
│ └── services
│ └── dynamodbv2
│ └── transactions
│ └── AwsCredentials.properties
├── pom.xml
└── src
├── main
└── java
│ └── com
│ └── amazonaws
│ └── services
│ └── dynamodbv2
│ ├── transactions
│ ├── ReadCommittedIsolationHandlerImpl.java
│ ├── ReadIsolationHandler.java
│ ├── ReadUncommittedIsolationHandlerImpl.java
│ ├── Request.java
│ ├── ThreadLocalDynamoDBFacade.java
│ ├── Transaction.java
│ ├── TransactionDynamoDBFacade.java
│ ├── TransactionItem.java
│ ├── TransactionManager.java
│ ├── TransactionManagerDynamoDBFacade.java
│ └── exceptions
│ │ ├── DuplicateRequestException.java
│ │ ├── InvalidRequestException.java
│ │ ├── ItemNotLockedException.java
│ │ ├── TransactionAssertionException.java
│ │ ├── TransactionCommittedException.java
│ │ ├── TransactionCompletedException.java
│ │ ├── TransactionException.java
│ │ ├── TransactionNotFoundException.java
│ │ ├── TransactionRolledBackException.java
│ │ └── UnknownCompletedTransactionException.java
│ └── util
│ ├── ImmutableAttributeValue.java
│ ├── ImmutableKey.java
│ └── TableHelper.java
└── test
└── java
└── com
└── amazonaws
└── services
└── dynamodbv2
├── transactions
├── ReadCommittedIsolationHandlerImplUnitTest.java
├── ReadUncommittedIsolationHandlerImplUnitTest.java
├── RequestTest.java
└── TransactionDynamoDBFacadeTest.java
└── util
└── ImmutableAttributeValueTest.java
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | *Issue #, if available:*
2 |
3 | *Description of changes:*
4 |
5 |
6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 | jdk:
3 | - openjdk7
4 | - oraclejdk7
5 | - openjdk6
6 | install: /bin/true
7 | script: mvn install --quiet -Dgpg.skip=true -DskipTests=true
8 |
9 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check [existing open](https://github.com/awslabs/dynamodb-transactions/issues), or [recently closed](https://github.com/awslabs/dynamodb-transactions/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *master* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/dynamodb-transactions/labels/help%20wanted) issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](https://github.com/awslabs/dynamodb-transactions/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
62 |
--------------------------------------------------------------------------------
/DESIGN.md:
--------------------------------------------------------------------------------
1 | # From CD to ACID: Adding Atomicity and Isolation to DynamoDB
2 |
3 | Out of the box, DynamoDB provides two of the four ACID properties: Consistency and Durability. Within a single item, you also get Atomicity and Isolation, but when your application needs to involve multiple items you lose those properties. Sometimes that's good enough, but many applications, especially distributed applications, would appreciate some of that Atomicity and Isolation as well. Fortunately, DynamoDB provides the tools (especially optimistic concurrency control) so that an application can achieve these properties and have full ACID transactions.
4 |
5 | This document outlines a technique for achieving atomic and (optionally) isolated transactions using DynamoDB. The atomicity strategy is a multi-phase commit protocol. To avoid losing data on coordinator failure, the coordinator state is maintained in DynamoDB. To avoid the need for failure detection, the protocol is designed so that there can be many active coordinators working on the same transaction. Isolation is available at various isolation levels, which is described later.
6 |
7 | The techniques described here are implemented and available for use as an extension of the AWS SDK for Java, and can be downloaded on GitHub.
8 |
9 | ## Part One: Atomic writes
10 |
11 | An atomic write transaction includes a set of update commands, each applied to a different DynamoDB item. The guarantee is that, when the transaction is complete, either all of the commands are executed, or none of them are.
12 |
13 | ### TX Records
14 |
15 | The state of an in-flight transaction is stored in a DynamoDB item called a TX record. It has these attributes:
16 |
17 | * a primary key. Any unique key will do, so a UUID is used.
18 | * a state. The state starts as pending, and is eventually updated to either committed or rolled-back.
19 | * a list of DynamoDB items participating in the transaction. Each item is given an id that is unique within the transaction.
20 | * a set of update commands. Each is an instruction for changing a DynamoDB item.
21 | * a timestamp indicating approximately when the transaction was last worked on.
22 | * a version number storing for detecting concurrent changes to a TX record.
23 |
24 | ### Locks
25 |
26 | Every DynamoDB item which participates in the protocol needs to have a lock attribute. Most of the time, this is set to the null string, but when the item is participating in a transaction, the lock is set to the primary key of the transaction's TX record. Each item can only participate in one transaction at a time. Additionally, each item participating in the transaction contains:
27 |
28 | * an applied flag, indicating whether the transaction has performed the write to the item yet.
29 | * a transient flag, indicating if the item was inserted in order to acquire the lock.
30 | * a timestamp indicating approximately when the item was locked.
31 |
32 | ### Item Images
33 |
34 | After each item is locked and before it is changed during the transaction, a complete previous copy of the item is kept in the transaction table. The requests in the transaction can be invalid, and their validity is not known until they are applied to the item, so we need a way to roll back a partially applied (but not committed) transaction. Examples of errors include an update request to add "1" to the string attribute "Foo". Each item image contains:
35 | * all attributes of the item before it was modified as a part of the transaction
36 | * the primary key of the transaction
37 | * the unique id of the request within the transaction
38 |
39 | Item images are not saved:
40 | * for items which are read lock requests
41 | * if the item is "transient" in the request, meaning that the item did not exist before it was locked
42 |
43 | ### No contention
44 |
45 | We start by considering the case of a transaction where none of the items are currently locked. In the next section, we'll add a dash of secret sauce to deal with contention. In this happy case, a coordinator executes a transaction by following these steps:
46 |
47 | * create. Insert a new TX record, ensuring that the primary key of the TX record is unique.
48 | * add. Add the item to the TX record's item list and assign it a unique request id. Also add the full update request for that item to the TX record. Use concurrency control to ensure that the TX record is still pending, and that it has not been changed since you last read or updated the TX record.
49 | * lock. Set the item's lock attribute to point to the TX record. If there is no item to lock, insert a new one, with a lock, and marked as transient. Use optimistic concurrency control to detect contention and to detect whether or not the item is transient.
50 | * save. Save a copy of the item in the transactions table, using optimistic concurrency control to avoid overwriting it. Skip this step if your copy is already marked as applied, if it is a read request, or the item is transient.
51 | * verify. Re-read the TX record to ensure it is still in the pending state.
52 | * apply. Perform the requested operation on the item. This changes the item before the transaction has committed, so clients who are reading the item without a read lock need to be aware that they are reading uncommitted writes. Delete requests are not actually performed at this step, since they would unlock the item, and the transaction has not committed yet.
53 | * commit. This is the key moment. The coordinator uses optimistic concurrency control to move the state of the TX record form pending to committed. For now, we ignore the case where some other coordinator rolls back the transaction due to contention.
54 | * complete. Once the transaction commits, complete it by deleting the old item images, and unlocking each item. If an item was marked as transient, or if the request for that item was a delete request, delete the item.
55 | * clean. Once the transaction is complete all locks are clear and the TX record is marked as complete.
56 | * delete. Once the caller has noted that the request is complete, and enough time has passed where the caller is confident that no other coordinator could be working on the transaction, the caller can delete the TX record.
57 |
58 | ### Contention with other transactions
59 |
60 | Contention happens when a lock attempt fails because that item is already part of some other transaction. To resolve the contention, the coordinator removes the lock. It does so by deciding the other transaction, and then completing it:
61 |
62 | * decide. Follow the lock to the transaction's TX record. If the transaction is pending, decide it by moving it from pending to rolled-back (using optimistic concurrency control, of course).
63 | * complete. If the transaction was committed, then use the same code as before to complete it, removing locks, and deleting transient items and all of the old item images. If the transaction was rolled back, then use the old item images to revert them, releasing their locks, and delete the old item images.
64 | * clean. All locks are now clear and the contending TX record is marked as complete. The creator of the conflicting transaction can use the TX record to determine if the transaction completed or rolled back.
65 | A problem with this aggressive approach is that coordinators can do battle, each rolling back the other other's transactions, and no one making much forward progress. This is a liveness issue, not a safety issue: the protocol as stated is correct. There are many techniques which can decrease contention. For example, a coordinator can pause before rolling back a transaction, giving the competing coordinator a chance to finish its work. This is especially useful if each coordinator acquires locks in the same order, so that deadlock is prevented. There are lots more techniques you can dream up: dreaming is left as an exercise to the reader.
66 | A bigger problem with this approach is that uncommitted reads are visible to the rest of the application if they are not using read locks. Read isolation is discussed later.
67 |
68 | ### Contention with other coordinators
69 |
70 | The protocol supports multiple coordinators working on the same transaction at the same time, including resuming a pending transaction, adding requests, and committing. To support this, the coordinator uses optimistic concurrency control when updating the TX record to ensure it makes valid state changes. Before applying an update to an item, the coordinator checks the state of the TX record to ensure it was not moved out of pending by another coordinator in between locking an item and applying the change. This interaction is most easily described by how a coordinator resumes a pending transaction that it did not start, or when contention is encountered with another coordinator of the same transaction:
71 |
72 | * read. Read the TX record
73 | * verify. Ensure that the transaction is in pending. If it is committed or rolled-back, drive the commit or rollback to completion.
74 | * catch up. Read each update from the TX record, and verify that each item is locked and backed up, and each update is applied using the algorithm above.
75 |
76 | ### Cleaning up
77 |
78 | Transaction items can be useful to leave around even when the transaction has been fully committed or rolled back. If a different coordinator completed a transaction than initiated it, they may want to leave the transaction record around so that the initiator can determine if their transaction was rolled back or successfully committed. If a competing transaction rolled back a transaction and then deleted the TX record, the caller would never be able to determine the fate of the transaction.
79 | One clean up approach is for transactions to be deleted only by the originator of the transaction. This would leave TX records around only when the original coordinator dies before it can delete the TX record.
80 |
81 | Another solution is to mark each transaction with the wall-clock time of last update, and delete transactions only once they have been completed, and have not been updated for some configured period of time. The application must run a sweeper process to periodically scan the transaction record for stuck or completed transactions, and move them along by rolling back and eventually deleting transaction records deemed "old enough". Clock skew and extreme delay in the application (such as persisting TX records outside of DynamoDB) can still cause confusion between coordinators, but is greatly reduced, and it doesn't affect correctness of the algorithm.
82 |
83 | ### Performance and scaling
84 |
85 | As implemented, this protocol requires 7N+4 writes. The 7N comes from: 3 for each item record for locking, making the change, and unlocking, 2 more saving and deleting each item's old image, and 2 more for each item to add each request to the TX record and later verify the TX record state. The extra 4 are to create the TX record, one to commit, one to mark it as finalized, and one to clean up. If desired, the transaction can be deleted instead of being marked as complete. This analysis assumes that each request in the transaction is an update to existing items. The algorithm is cheaper for obtaining read locks and inserting new items, since in these cases the old item images do not need to be saved.
86 |
87 | The protocol will scale to any transaction rate, thanks to DynamoDB's behind-the-scenes partitioning. Two unrelated transactions do not interfere with one another. The table of TX records can be indexed using a hash key, which provides nearly unlimited scaling.
88 |
89 | As defined, the protocol will not scale to transactions with a large number of update commands. That's because the TX record, which must hold all the update commands, is limited, like any DynamoDB item, to 400KB. One possible fix is to use a hash plus range key for the TX table, where one of the records (say the record with range key zero) is the TX record itself, and the other records in the range represent the items and updates.
90 |
91 | ## Part Two: Isolation
92 |
93 | So far, we have not discussed reads. There are several approaches for incorporating read isolation with this algorithm, including ones not covered here. This library provides 3 different read isolation levels:
94 |
95 | The simplest approach is just to ignore locks. The problem is that this does not provide isolation: read transactions can see partial writes, and even uncommitted writes which may be rolled back, since the algorithm has to apply changes before commit in order to see if the transaction even can commit.
96 |
97 | A stronger form of isolation is similar to what DynamoDB offers today: where you are guaranteed to read only committed changes, but without a "consistent cut", meaning that you could read some items from before a transaction commits and other items from after it commits. This is accomplished by taking advantage of the fact that the algorithm saves the old item image away before it applies changes. The item is read directly, without taking a lock, and if the item is marked as locked by another transaction and "applied", then the old item image is read instead. This approach doesn't guarantee that you are returned the latest committed version of the item since the transaction locking the item can be committed, but not unlocked yet. However it avoids the pitfalls of the weakest read consistency style by returning only item states that were committed.
98 |
99 | The strongest form of read isolation is use read locks. An easy implementation is to code a read transaction exactly like a write, except that at the conclusion of the transaction you always roll back. This provides full ACID semantics, at the cost of turning reads into relatively expensive writes. Still, it scales, it's simple, and DynamoDB is so fast that this approach will be suitable for many applications.
100 |
101 | ## Limitations
102 |
103 | The protocol described here has some problems. These limitations include:
104 |
105 | ### Range queries
106 |
107 | There is no provision for locking ranges, so transaction which include range queries are subject to phantom reads. This could be solved by storing locks for specific ranges in a new item with the same hash key value as the range.
108 |
109 | ### Cost
110 |
111 | There are cheaper approaches to performing transactions on DynamoDB, but they each come with their own set of limitations in terms of capability. Some approaches rely on a global clock, which is a scaling bottleneck acceptable to some applications, while other approaches do not have the ability to handle bad requests to DynamoDB, or can only be scoped to items in a "parent/child" relationship.
112 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
--------------------------------------------------------------------------------
/META-INF/MANIFEST.MF:
--------------------------------------------------------------------------------
1 | Manifest-Version: 1.0
2 | Bundle-ManifestVersion: 2
3 | Bundle-Name: Amazon DynamoDB Transactions on the AWS SDK for Java
4 | Bundle-SymbolicName: com.amazonaws.services.dynamodbv2;singleton:=true
5 | Bundle-Version: 1.0.0
6 | Bundle-Vendor: Amazon Technologies, Inc
7 | Bundle-RequiredExecutionEnvironment: JavaSE-1.6
8 | Export-Package: com.amazonaws.services.dynamodbv2.transactions,
9 | com.amazonaws.services.dynamodbv2.transactions.examples,
10 | com.amazonaws.services.dynamodbv2.transactions.exceptions
11 |
--------------------------------------------------------------------------------
/NOTICE.txt:
--------------------------------------------------------------------------------
1 | Amazon DynamoDB Transactions for the AWS SDK for Java
2 | Copyright 2013-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Transactions for Amazon DynamoDB
2 |
3 | **_[IMPORTANT]_ Since November 2018, DynamoDB offers transactional APIs, simplifying the developer experience of making coordinated, all-or-nothing changes to multiple items both within and across tables. DynamoDB Transactions provide atomicity, consistency, isolation, and durability (ACID) in DynamoDB, enabling you to maintain data correctness in your applications more easily. We strongly recommend all developers to use DynamoDB’s built-in, servers-side transactions instead of this client-side library. To learn more about DynamoDB Transactions, see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transactions.html.**
4 |
5 |
6 | **Amazon DynamoDB Client-Side Transactions** enables Java developers to easily perform atomic writes and isolated reads across multiple items and tables when building high scale applications on [Amazon DynamoDB][dynamodb]. You can get started in minutes using ***Maven***.
7 |
8 | * [Transactions Details & Design][design]
9 | * [DynamoDB Forum][sdk-forum]
10 | * [Transactions Library Issues][sdk-issues]
11 |
12 | The **Amazon DynamoDB Client-Side Transactions** library is built on top of the low-level Amazon DynamoDB client in the AWS SDK for Java. For support in using and installing the AWS SDK for Java, see:
13 |
14 | * [API Docs][docs-api]
15 | * [SDK Developer Guide][docs-guide]
16 | * [AWS SDK Forum][sdk-forum]
17 | * [SDK Homepage][sdk-website]
18 | * [Java Development AWS Blog][sdk-blog]
19 |
20 | ## Features
21 |
22 | * **Atomic writes:** Write operations to multiple items are either all go through, or none go through.
23 | * **Isolated reads:** Read operations to multiple items are not interfered with by other transactions.
24 | * **Sweepers:** In-flight transaction state is stored in a separate table, and convenience methods are provided to "sweep" this table for "stuck" transactions.
25 | * **Easy to use:** Mimics the Amazon DynamoDB API by using the request and response objects from the low-level APIs, including PutItem, UpdateItem, DeleteItem, and GetItem.
26 | * **Table helpers:** Includes useful methods for creating tables such as and waiting for them to become ACTIVE.
27 |
28 | ## Getting Started
29 |
30 | 1. **Sign up for AWS** - Before you begin, you need an AWS account. Please see the [AWS Account and Credentials][docs-signup] section of the developer guide for information about how to create an AWS account and retrieve your AWS credentials.
31 | 1. **Minimum requirements** - To run the SDK you will need **Java 1.6+**. For more information about the requirements and optimum settings for the SDK, please see the [Java Development Environment][docs-signup] section of the developer guide.
32 | 1. **Install the Amazon DynamoDB Transactions Library** - Using ***Maven*** is the recommended way to install the Amazon DynamoDB Transactions Library and its dependencies, including the AWS SDK for Java. To download the code from GitHub, simply clone the repository by typing: `git clone https://github.com/awslabs/dynamodb-transactions`, and run the Maven command described below in "Building From Source".
33 | 1. **Run the examples** - The included *TransactionExamples* automatically creates the necessary transactions tables, an example table for data and executes several operations with transactions. You can run the examples using Maven by:
34 | 1. Ensure you have already built the library using Maven (see "Building From Source" below)
35 | 2. Change into the *examples* directory of the project
36 | 2. Add your AWS Credentials to the file: *src/main/resources/com/amazonaws/services/dynamodbv2/transactions/examples/AwsCredentials.properties*
37 | 3. Compile the subproject by typing: `mvn clean install`
38 | 4. Run the examples by typing: `mvn exec:java -Dexec.mainClass="com.amazonaws.services.dynamodbv2.transactions.examples.TransactionExamples"`
39 |
40 | ## Building From Source
41 |
42 | Once you check out the code from GitHub, you can build it using Maven. To disable the GPG-signing in the build, use: `mvn clean install -Dgpg.skip=true`
43 |
44 | [design]: https://github.com/awslabs/dynamodb-transactions/blob/master/DESIGN.md
45 | [sdk-install-jar]: http://sdk-for-java.amazonwebservices.com/latest/aws-java-sdk.zip
46 | [aws]: http://aws.amazon.com/
47 | [dynamodb]: http://aws.amazon.com/dynamodb
48 | [dynamodb-forum]: https://forums.aws.amazon.com/forum.jspa?forumID=131
49 | [sdk-website]: http://aws.amazon.com/sdkforjava
50 | [sdk-forum]: http://developer.amazonwebservices.com/connect/forum.jspa?forumID=70
51 | [sdk-blog]: https://java.awsblog.com/
52 | [sdk-issues]: https://github.com/awslabs/dynamodb-transactions/issues
53 | [sdk-license]: http://www.apache.org/licenses/LICENSE-2.0
54 | [docs-api]: http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/index.html
55 | [docs-dynamodb-api]: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/Welcome.html
56 | [docs-dynamodb]: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide
57 | [docs-signup]: http://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-setup.html
58 | [aws-iam-credentials]: http://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-roles.html
59 | [docs-guide]: http://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/welcome.html
60 |
--------------------------------------------------------------------------------
/build.properties:
--------------------------------------------------------------------------------
1 | source.. = src/main/java
2 | output.. = bin/
3 |
4 | bin.includes = LICENSE.txt,\
5 | NOTICE.txt,\
6 | META-INF/,\
7 | .
8 |
9 | jre.compilation.profile = JavaSE-1.6
--------------------------------------------------------------------------------
/examples/pom.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 | 4.0.0
6 | com.amazonaws.services.dynamodbv2
7 | amazon-dynamodb-transactions-examples
8 | jar
9 | Examples for the Amazon DynamoDB Transactions on the AWS SDK for Java
10 | 1.1.2
11 | Runnable examples of using transactions
12 | https://aws.amazon.com/dynamodb
13 |
14 |
15 | https://github.com/awslabs/dynamodb-transactions.git
16 |
17 |
18 |
19 |
20 | Apache 2.0 License
21 | http://www.apache.org/licenses/LICENSE-2.0
22 | repo
23 |
24 |
25 |
26 |
27 |
28 | com.amazonaws.services.dynamodbv2
29 | amazon-dynamodb-transactions
30 | 1.1.2
31 |
32 |
33 |
34 |
35 |
36 | amazonwebservices
37 | Amazon Web Services
38 | https://aws.amazon.com
39 |
40 | developer
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | org.apache.maven.plugins
50 | maven-compiler-plugin
51 |
52 | 1.6
53 | 1.6
54 | UTF-8
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/examples/src/main/resources/com/amazonaws/services/dynamodbv2/transactions/examples/AwsCredentials.properties:
--------------------------------------------------------------------------------
1 | # Fill in your AWS Access Key ID and Secret Access Key
2 | # http://aws.amazon.com/security-credentials
3 | accessKey =
4 | secretKey =
5 |
--------------------------------------------------------------------------------
/integration/pom.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 | 4.0.0
6 | com.amazonaws.services.dynamodbv2
7 | amazon-dynamodb-transactions-integration
8 | jar
9 | Integration tests for the Amazon DynamoDB Transactions on the AWS SDK for Java
10 | 1.1.2
11 | Integration tests, requiring AWS credentials
12 | https://aws.amazon.com/dynamodb
13 |
14 |
15 | https://github.com/awslabs/dynamodb-transactions.git
16 |
17 |
18 |
19 |
20 | Apache 2.0 License
21 | http://www.apache.org/licenses/LICENSE-2.0
22 | repo
23 |
24 |
25 |
26 |
27 |
28 |
29 | com.amazonaws.services.dynamodbv2
30 | amazon-dynamodb-transactions
31 | 1.1.2
32 |
33 |
34 |
35 |
36 | junit
37 | junit
38 | 4.13.1
39 | true
40 |
41 |
42 |
43 |
44 |
45 | amazonwebservices
46 | Amazon Web Services
47 | https://aws.amazon.com
48 |
49 | developer
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | org.apache.maven.plugins
59 | maven-compiler-plugin
60 |
61 | 1.6
62 | 1.6
63 | UTF-8
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/integration/src/test/java/com/amazonaws/services/dynamodbv2/transactions/FailingAmazonDynamoDBClient.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2013-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.amazonaws.services.dynamodbv2.transactions;
6 |
7 | import java.util.HashMap;
8 | import java.util.HashSet;
9 | import java.util.Map;
10 | import java.util.Queue;
11 | import java.util.Set;
12 |
13 | import com.amazonaws.AmazonClientException;
14 | import com.amazonaws.AmazonServiceException;
15 | import com.amazonaws.AmazonWebServiceRequest;
16 | import com.amazonaws.auth.AWSCredentials;
17 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
18 | import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
19 | import com.amazonaws.services.dynamodbv2.model.GetItemResult;
20 | import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest;
21 | import com.amazonaws.services.dynamodbv2.model.UpdateItemResult;
22 |
23 | /**
24 | * A very primitive fault-injection client.
25 | *
26 | * @author dyanacek
27 | */
28 | public class FailingAmazonDynamoDBClient extends AmazonDynamoDBClient {
29 |
30 | public static class FailedYourRequestException extends RuntimeException {
31 | private static final long serialVersionUID = -7191808024168281212L;
32 | }
33 |
34 | // Any requests added to this set will throw a FailedYourRequestException when called.
35 | public final Set requestsToFail = new HashSet();
36 |
37 | // Any requests added to this set will return a null item when called
38 | public final Set getRequestsToTreatAsDeleted = new HashSet();
39 |
40 | // Any requests with keys in this set will return the queue of responses in order. When the end of the queue is reached
41 | // further requests will be passed to the DynamoDB client.
42 | public final Map> getRequestsToStub = new HashMap>();
43 |
44 | /**
45 | * Resets the client to the stock DynamoDB client (all requests will call DynamoDB)
46 | */
47 | public void reset() {
48 | requestsToFail.clear();
49 | getRequestsToTreatAsDeleted.clear();
50 | getRequestsToStub.clear();
51 | }
52 |
53 | public FailingAmazonDynamoDBClient(AWSCredentials credentials) {
54 | super(credentials);
55 | }
56 |
57 | @Override
58 | public GetItemResult getItem(GetItemRequest getItemRequest) throws AmazonServiceException, AmazonClientException {
59 | if(requestsToFail.contains(getItemRequest)) {
60 | throw new FailedYourRequestException();
61 | }
62 | if (getRequestsToTreatAsDeleted.contains(getItemRequest)) {
63 | return new GetItemResult();
64 | }
65 | Queue stubbedResults = getRequestsToStub.get(getItemRequest);
66 | if (stubbedResults != null && !stubbedResults.isEmpty()) {
67 | return stubbedResults.remove();
68 | }
69 | return super.getItem(getItemRequest);
70 | }
71 |
72 | @Override
73 | public UpdateItemResult updateItem(UpdateItemRequest updateItemRequest) throws AmazonServiceException,
74 | AmazonClientException {
75 | if(requestsToFail.contains(updateItemRequest)) {
76 | throw new FailedYourRequestException();
77 | }
78 | return super.updateItem(updateItemRequest);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/integration/src/test/java/com/amazonaws/services/dynamodbv2/transactions/IntegrationTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.amazonaws.services.dynamodbv2.transactions;
6 |
7 | import com.amazonaws.auth.AWSCredentials;
8 | import com.amazonaws.auth.BasicAWSCredentials;
9 | import com.amazonaws.auth.PropertiesCredentials;
10 | import com.amazonaws.auth.profile.ProfileCredentialsProvider;
11 | import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig;
12 | import com.amazonaws.services.dynamodbv2.document.DynamoDB;
13 | import com.amazonaws.services.dynamodbv2.document.Table;
14 | import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
15 | import com.amazonaws.services.dynamodbv2.model.AttributeValue;
16 | import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
17 | import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
18 | import com.amazonaws.services.dynamodbv2.model.GetItemResult;
19 | import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
20 | import com.amazonaws.services.dynamodbv2.model.KeyType;
21 | import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
22 | import com.amazonaws.services.dynamodbv2.model.ResourceInUseException;
23 | import com.amazonaws.services.dynamodbv2.model.ReturnConsumedCapacity;
24 | import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
25 | import com.amazonaws.services.dynamodbv2.transactions.exceptions.TransactionNotFoundException;
26 | import com.amazonaws.services.dynamodbv2.util.ImmutableKey;
27 | import org.junit.AfterClass;
28 | import org.junit.BeforeClass;
29 | import org.junit.Ignore;
30 |
31 | import java.io.IOException;
32 | import java.text.SimpleDateFormat;
33 | import java.util.Date;
34 | import java.util.HashMap;
35 | import java.util.Map;
36 |
37 | import static org.junit.Assert.assertEquals;
38 | import static org.junit.Assert.assertFalse;
39 | import static org.junit.Assert.assertNotNull;
40 | import static org.junit.Assert.assertNull;
41 | import static org.junit.Assert.assertTrue;
42 | import static org.junit.Assert.fail;
43 |
44 | @Ignore
45 | public class IntegrationTest {
46 |
47 | protected static final FailingAmazonDynamoDBClient dynamodb;
48 | protected static final DynamoDB documentDynamoDB;
49 | private static final String DYNAMODB_ENDPOINT = "http://dynamodb.us-west-2.amazonaws.com";
50 | private static final String DYNAMODB_ENDPOINT_PROPERTY = "dynamodb-local.endpoint";
51 |
52 | protected static final String ID_ATTRIBUTE = "Id";
53 | protected static final String HASH_TABLE_NAME = "TransactionsIntegrationTest_Hash";
54 | protected static final String HASH_RANGE_TABLE_NAME = "TransactionsIntegrationTest_HashRange";
55 | protected static final String LOCK_TABLE_NAME = "TransactionsIntegrationTest_Transactions";
56 | protected static final String IMAGES_TABLE_NAME = "TransactionsIntegrationTest_ItemImages";
57 | protected static final String TABLE_NAME_PREFIX = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss").format(new Date());
58 |
59 | protected static final String INTEG_LOCK_TABLE_NAME = TABLE_NAME_PREFIX + "_" + LOCK_TABLE_NAME;
60 | protected static final String INTEG_IMAGES_TABLE_NAME = TABLE_NAME_PREFIX + "_" + IMAGES_TABLE_NAME;
61 | protected static final String INTEG_HASH_TABLE_NAME = TABLE_NAME_PREFIX + "_" + HASH_TABLE_NAME;
62 | protected static final String INTEG_HASH_RANGE_TABLE_NAME = TABLE_NAME_PREFIX + "_" + HASH_RANGE_TABLE_NAME;
63 |
64 | protected final TransactionManager manager;
65 |
66 | public IntegrationTest() {
67 | manager = new TransactionManager(dynamodb, INTEG_LOCK_TABLE_NAME, INTEG_IMAGES_TABLE_NAME);
68 | }
69 |
70 | public IntegrationTest(DynamoDBMapperConfig config) {
71 | manager = new TransactionManager(dynamodb, INTEG_LOCK_TABLE_NAME, INTEG_IMAGES_TABLE_NAME, config);
72 | }
73 |
74 | static {
75 | AWSCredentials credentials;
76 | String endpoint = System.getProperty(DYNAMODB_ENDPOINT_PROPERTY);
77 | if (endpoint != null) {
78 | credentials = new BasicAWSCredentials("local", "local");
79 | } else {
80 | endpoint = DYNAMODB_ENDPOINT;
81 | try {
82 | credentials = new PropertiesCredentials(
83 | TransactionsIntegrationTest.class.getResourceAsStream("AwsCredentials.properties"));
84 | if(credentials.getAWSAccessKeyId().isEmpty()) {
85 | System.err.println("No credentials supplied in AwsCredentials.properties, will try with default credentials file");
86 | credentials = new ProfileCredentialsProvider().getCredentials();
87 | }
88 | } catch (IOException e) {
89 | System.err.println("Could not load credentials from built-in credentials file.");
90 | throw new RuntimeException(e);
91 | }
92 | }
93 |
94 | dynamodb = new FailingAmazonDynamoDBClient(credentials);
95 | dynamodb.setEndpoint(endpoint);
96 |
97 | documentDynamoDB = new DynamoDB(dynamodb);
98 | }
99 |
100 | protected Map key0;
101 | protected Map item0;
102 |
103 | protected Map newKey(String tableName) {
104 | Map key = new HashMap();
105 | key.put(ID_ATTRIBUTE, new AttributeValue().withS("val_" + Math.random()));
106 | if (INTEG_HASH_RANGE_TABLE_NAME.equals(tableName)) {
107 | key.put("RangeAttr", new AttributeValue().withN(Double.toString(Math.random())));
108 | } else if(!INTEG_HASH_TABLE_NAME.equals(tableName)){
109 | throw new IllegalArgumentException();
110 | }
111 | return key;
112 | }
113 |
114 | private static void waitForTableToBecomeAvailable(String tableName) {
115 | Table tableToWaitFor = documentDynamoDB.getTable(tableName);
116 | try {
117 | System.out.println("Waiting for " + tableName + " to become ACTIVE...");
118 | tableToWaitFor.waitForActive();
119 | } catch (Exception e) {
120 | throw new RuntimeException("Table " + tableName + " never went active");
121 | }
122 | }
123 |
124 | @BeforeClass
125 | public static void createTables() throws InterruptedException {
126 | try {
127 | CreateTableRequest createHash = new CreateTableRequest()
128 | .withTableName(INTEG_HASH_TABLE_NAME)
129 | .withAttributeDefinitions(new AttributeDefinition().withAttributeName(ID_ATTRIBUTE).withAttributeType(ScalarAttributeType.S))
130 | .withKeySchema(new KeySchemaElement().withAttributeName(ID_ATTRIBUTE).withKeyType(KeyType.HASH))
131 | .withProvisionedThroughput(new ProvisionedThroughput().withReadCapacityUnits(5L).withWriteCapacityUnits(5L));
132 | dynamodb.createTable(createHash);
133 | } catch (ResourceInUseException e) {
134 | System.err.println("Warning: " + INTEG_HASH_TABLE_NAME + " was already in use");
135 | }
136 |
137 | try {
138 | TransactionManager.verifyOrCreateTransactionTable(dynamodb, INTEG_LOCK_TABLE_NAME, 10L, 10L, 5L * 60);
139 | TransactionManager.verifyOrCreateTransactionImagesTable(dynamodb, INTEG_IMAGES_TABLE_NAME, 10L, 10L, 5L * 60);
140 | } catch (ResourceInUseException e) {
141 | System.err.println("Warning: " + INTEG_HASH_TABLE_NAME + " was already in use");
142 | }
143 |
144 | waitForTableToBecomeAvailable(INTEG_HASH_TABLE_NAME);
145 | waitForTableToBecomeAvailable(INTEG_LOCK_TABLE_NAME);
146 | waitForTableToBecomeAvailable(INTEG_IMAGES_TABLE_NAME);
147 | }
148 |
149 | @AfterClass
150 | public static void deleteTables() throws InterruptedException {
151 | try {
152 | Table hashTable = documentDynamoDB.getTable(INTEG_HASH_TABLE_NAME);
153 | Table lockTable = documentDynamoDB.getTable(INTEG_LOCK_TABLE_NAME);
154 | Table imagesTable = documentDynamoDB.getTable(INTEG_IMAGES_TABLE_NAME);
155 |
156 | System.out.println("Issuing DeleteTable request for " + INTEG_HASH_TABLE_NAME);
157 | hashTable.delete();
158 | System.out.println("Issuing DeleteTable request for " + INTEG_LOCK_TABLE_NAME);
159 | lockTable.delete();
160 | System.out.println("Issuing DeleteTable request for " + INTEG_IMAGES_TABLE_NAME);
161 | imagesTable.delete();
162 |
163 | System.out.println("Waiting for " + INTEG_HASH_TABLE_NAME + " to be deleted...this may take a while...");
164 | hashTable.waitForDelete();
165 | System.out.println("Waiting for " + INTEG_LOCK_TABLE_NAME + " to be deleted...this may take a while...");
166 | lockTable.waitForDelete();
167 | System.out.println("Waiting for " + INTEG_IMAGES_TABLE_NAME + " to be deleted...this may take a while...");
168 | imagesTable.waitForDelete();
169 | } catch (Exception e) {
170 | System.err.println("DeleteTable request failed for some table");
171 | System.err.println(e.getMessage());
172 | }
173 | }
174 |
175 | protected void assertItemLocked(String tableName, Map key, Map expected, String owner, boolean isTransient, boolean isApplied) {
176 | assertItemLocked(tableName, key, expected, owner, isTransient, isApplied, true /*checkTxItem*/);
177 | }
178 |
179 | protected void assertItemLocked(String tableName, Map key, Map expected, String owner, boolean isTransient, boolean isApplied, boolean checkTxItem) {
180 | Map item = getItem(tableName, key);
181 | assertNotNull(item);
182 | assertEquals(owner, item.get(Transaction.AttributeName.TXID.toString()).getS());
183 | if(isTransient) {
184 | assertTrue("item is not transient, and should have been", item.containsKey(Transaction.AttributeName.TRANSIENT.toString()));
185 | assertEquals("item is not transient, and should have been", "1", item.get(Transaction.AttributeName.TRANSIENT.toString()).getS());
186 | } else {
187 | assertNull("item is transient, and should not have been", item.get(Transaction.AttributeName.TRANSIENT.toString()));
188 | }
189 | if(isApplied) {
190 | assertTrue("item is not applied, and should have been", item.containsKey(Transaction.AttributeName.APPLIED.toString()));
191 | assertEquals("item is not applied, and should have been", "1", item.get(Transaction.AttributeName.APPLIED.toString()).getS());
192 | } else {
193 | assertNull("item is applied, and should not have been", item.get(Transaction.AttributeName.APPLIED.toString()));
194 | }
195 | assertTrue(item.containsKey(Transaction.AttributeName.DATE.toString()));
196 | if(expected != null) {
197 | item.remove(Transaction.AttributeName.TXID.toString());
198 | item.remove(Transaction.AttributeName.TRANSIENT.toString());
199 | item.remove(Transaction.AttributeName.APPLIED.toString());
200 | item.remove(Transaction.AttributeName.DATE.toString());
201 | assertEquals(expected, item);
202 | }
203 | // Also verify that it is locked in the tx record
204 | if(checkTxItem) {
205 | TransactionItem txItem = new TransactionItem(owner, manager, false /*insert*/);
206 | assertTrue(txItem.getRequestMap().containsKey(tableName));
207 | assertTrue(txItem.getRequestMap().get(tableName).containsKey(new ImmutableKey(key)));
208 | }
209 | }
210 |
211 | protected void assertItemLocked(String tableName, Map key, String owner, boolean isTransient, boolean isApplied) {
212 | assertItemLocked(tableName, key, null /*expected*/, owner, isTransient, isApplied);
213 | }
214 |
215 | protected void assertItemNotLocked(String tableName, Map key, Map expected, boolean shouldExist) {
216 | Map item = getItem(tableName, key);
217 | if(shouldExist) {
218 | assertNotNull("Item does not exist in the table, but it should", item);
219 | assertNull(item.get(Transaction.AttributeName.TRANSIENT.toString()));
220 | assertNull(item.get(Transaction.AttributeName.TXID.toString()));
221 | assertNull(item.get(Transaction.AttributeName.APPLIED.toString()));
222 | assertNull(item.get(Transaction.AttributeName.DATE.toString()));
223 | } else {
224 | assertNull("Item should have been null: " + item, item);
225 | }
226 |
227 | if(expected != null) {
228 | item.remove(Transaction.AttributeName.TXID.toString());
229 | item.remove(Transaction.AttributeName.TRANSIENT.toString());
230 | assertEquals(expected, item);
231 | }
232 | }
233 |
234 | protected void assertItemNotLocked(String tableName, Map key, boolean shouldExist) {
235 | assertItemNotLocked(tableName, key, null, shouldExist);
236 | }
237 |
238 | protected void assertTransactionDeleted(Transaction t) {
239 | try {
240 | manager.resumeTransaction(t.getId());
241 | fail();
242 | } catch (TransactionNotFoundException e) {
243 | assertTrue(e.getMessage().contains("Transaction not found"));
244 | }
245 | }
246 |
247 | protected void assertNoSpecialAttributes(Map item) {
248 | for(String attrName : Transaction.SPECIAL_ATTR_NAMES) {
249 | if(item.containsKey(attrName)) {
250 | fail("Should not have contained attribute " + attrName + " " + item);
251 | }
252 | }
253 | }
254 |
255 | protected void assertOldItemImage(String txId, String tableName, Map key, Map item, boolean shouldExist) {
256 | Transaction t = manager.resumeTransaction(txId);
257 | Map> requests = t.getTxItem().getRequestMap();
258 | Request r = requests.get(tableName).get(new ImmutableKey(key));
259 | Map image = t.getTxItem().loadItemImage(r.getRid());
260 | if(shouldExist) {
261 | assertNotNull(image);
262 | image.remove(Transaction.AttributeName.TXID.toString());
263 | image.remove(Transaction.AttributeName.IMAGE_ID.toString());
264 | image.remove(Transaction.AttributeName.DATE.toString());
265 | assertFalse(image.containsKey(Transaction.AttributeName.TRANSIENT.toString()));
266 | assertEquals(item, image); // TODO does not work for Set AttributeValue types (DynamoDB does not preserve ordering)
267 | } else {
268 | assertNull(image);
269 | }
270 | }
271 |
272 | protected Map getItem(String tableName, Map key) {
273 | GetItemResult result = dynamodb.getItem(new GetItemRequest()
274 | .withTableName(tableName)
275 | .withKey(key)
276 | .withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL)
277 | .withConsistentRead(true));
278 | return result.getItem();
279 | }
280 |
281 | }
282 |
--------------------------------------------------------------------------------
/integration/src/test/java/com/amazonaws/services/dynamodbv2/transactions/TransactionManagerDBFacadeIntegrationTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2013-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.amazonaws.services.dynamodbv2.transactions;
7 |
8 | import com.amazonaws.services.dynamodbv2.model.AttributeValue;
9 | import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate;
10 | import com.amazonaws.services.dynamodbv2.model.BatchGetItemRequest;
11 | import com.amazonaws.services.dynamodbv2.model.BatchGetItemResult;
12 | import com.amazonaws.services.dynamodbv2.model.ComparisonOperator;
13 | import com.amazonaws.services.dynamodbv2.model.Condition;
14 | import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
15 | import com.amazonaws.services.dynamodbv2.model.GetItemResult;
16 | import com.amazonaws.services.dynamodbv2.model.KeysAndAttributes;
17 | import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
18 | import com.amazonaws.services.dynamodbv2.model.QueryRequest;
19 | import com.amazonaws.services.dynamodbv2.model.QueryResult;
20 | import com.amazonaws.services.dynamodbv2.model.ScanRequest;
21 | import com.amazonaws.services.dynamodbv2.model.ScanResult;
22 | import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest;
23 | import org.junit.After;
24 | import org.junit.Before;
25 | import org.junit.Test;
26 |
27 | import java.util.Arrays;
28 | import java.util.Collections;
29 | import java.util.HashMap;
30 | import java.util.List;
31 | import java.util.Map;
32 |
33 | import static org.junit.Assert.assertEquals;
34 | import static org.junit.Assert.assertFalse;
35 | import static org.junit.Assert.assertNotNull;
36 |
37 | public class TransactionManagerDBFacadeIntegrationTest extends IntegrationTest {
38 |
39 | private TransactionManagerDynamoDBFacade uncommittedFacade;
40 | private TransactionManagerDynamoDBFacade committedFacade;
41 |
42 | private Map update;
43 | private Map item0Updated;
44 | private Map item0Filtered; // item0 with only the attributesToGet
45 | private List attributesToGet;
46 |
47 | public TransactionManagerDBFacadeIntegrationTest() {
48 | super();
49 | }
50 |
51 | @Before
52 | public void setup() {
53 | dynamodb.reset();
54 | uncommittedFacade = new TransactionManagerDynamoDBFacade(manager, Transaction.IsolationLevel.UNCOMMITTED);
55 | committedFacade = new TransactionManagerDynamoDBFacade(manager, Transaction.IsolationLevel.COMMITTED);
56 | key0 = newKey(INTEG_HASH_TABLE_NAME);
57 | item0 = new HashMap(key0);
58 | item0.put("s_someattr", new AttributeValue("val"));
59 | item0Filtered = new HashMap(item0);
60 | item0.put("attr_not_to_get", new AttributeValue("val_not_to_get"));
61 | attributesToGet = Arrays.asList(ID_ATTRIBUTE, "s_someattr"); // not including attr_not_to_get
62 | update = Collections.singletonMap(
63 | "s_someattr",
64 | new AttributeValueUpdate().withValue(new AttributeValue("val2")));
65 | item0Updated = new HashMap(item0);
66 | item0Updated.put("s_someattr", new AttributeValue("val2"));
67 | }
68 |
69 | @After
70 | public void cleanup() throws InterruptedException {
71 | deleteTables();
72 | createTables();
73 | dynamodb.reset();
74 | }
75 |
76 | private void putItem(final boolean commit) {
77 | Transaction t = manager.newTransaction();
78 | t.putItem(new PutItemRequest()
79 | .withTableName(INTEG_HASH_TABLE_NAME)
80 | .withItem(item0));
81 | if (commit) {
82 | t.commit();
83 | assertItemNotLocked(INTEG_HASH_TABLE_NAME, key0, true);
84 | } else {
85 | assertItemLocked(INTEG_HASH_TABLE_NAME, key0, t.getId(), true, true);
86 | }
87 | }
88 |
89 | private void updateItem(final boolean commit) {
90 | Transaction t = manager.newTransaction();
91 | UpdateItemRequest request = new UpdateItemRequest()
92 | .withTableName(INTEG_HASH_TABLE_NAME)
93 | .withKey(key0)
94 | .withAttributeUpdates(update);
95 | t.updateItem(request);
96 | if (commit) {
97 | t.commit();
98 | assertItemNotLocked(INTEG_HASH_TABLE_NAME, key0, true);
99 | } else {
100 | assertItemLocked(INTEG_HASH_TABLE_NAME, key0, t.getId(), false, true);
101 | }
102 | }
103 |
104 | private void assertContainsNoTransactionAttributes(final Map item) {
105 | assertFalse(Transaction.isLocked(item));
106 | assertFalse(Transaction.isApplied(item));
107 | assertFalse(Transaction.isTransient(item));
108 | }
109 |
110 | private QueryRequest createQueryRequest(final boolean filterAttributes) {
111 | Condition hashKeyCondition = new Condition()
112 | .withComparisonOperator(ComparisonOperator.EQ)
113 | .withAttributeValueList(key0.get(ID_ATTRIBUTE));
114 | QueryRequest request = new QueryRequest()
115 | .withTableName(INTEG_HASH_TABLE_NAME)
116 | .withKeyConditions(Collections.singletonMap(ID_ATTRIBUTE, hashKeyCondition));
117 | if (filterAttributes) {
118 | request.setAttributesToGet(attributesToGet);
119 | }
120 | return request;
121 | }
122 |
123 | private BatchGetItemRequest createBatchGetItemRequest(final boolean filterAttributes) {
124 | KeysAndAttributes keysAndAttributes = new KeysAndAttributes()
125 | .withKeys(key0);
126 | if (filterAttributes) {
127 | keysAndAttributes.withAttributesToGet(attributesToGet);
128 | }
129 | return new BatchGetItemRequest()
130 | .withRequestItems(
131 | Collections.singletonMap(
132 | INTEG_HASH_TABLE_NAME,
133 | keysAndAttributes));
134 | }
135 |
136 | private void testGetItemContainsItem(
137 | final TransactionManagerDynamoDBFacade facade,
138 | final Map item,
139 | final boolean filterAttributes) {
140 | GetItemRequest request = new GetItemRequest()
141 | .withTableName(INTEG_HASH_TABLE_NAME)
142 | .withKey(key0);
143 | if (filterAttributes) {
144 | request.setAttributesToGet(attributesToGet);
145 | }
146 | GetItemResult result = facade.getItem(request);
147 | assertContainsNoTransactionAttributes(result.getItem());
148 | assertEquals(item, result.getItem());
149 | }
150 |
151 | private void testScanContainsItem(
152 | final TransactionManagerDynamoDBFacade facade,
153 | final Map item,
154 | final boolean filterAttributes) {
155 | ScanRequest scanRequest = new ScanRequest()
156 | .withTableName(INTEG_HASH_TABLE_NAME);
157 | if (filterAttributes) {
158 | scanRequest.setAttributesToGet(attributesToGet);
159 | }
160 | ScanResult scanResult = facade.scan(scanRequest);
161 | assertEquals(1, scanResult.getItems().size());
162 | assertContainsNoTransactionAttributes(scanResult.getItems().get(0));
163 | assertEquals(item, scanResult.getItems().get(0));
164 | }
165 |
166 | private void testScanIsEmpty(final TransactionManagerDynamoDBFacade facade) {
167 | ScanResult scanResult = facade.scan(new ScanRequest()
168 | .withTableName(INTEG_HASH_TABLE_NAME));
169 | assertNotNull(scanResult.getItems());
170 | assertEquals(0, scanResult.getItems().size());
171 | }
172 |
173 | private void testQueryContainsItem(
174 | final TransactionManagerDynamoDBFacade facade,
175 | final Map item,
176 | final boolean filterAttributes) {
177 | QueryRequest queryRequest = createQueryRequest(filterAttributes);
178 | QueryResult queryResult = facade.query(queryRequest);
179 | assertEquals(1, queryResult.getItems().size());
180 | assertContainsNoTransactionAttributes(queryResult.getItems().get(0));
181 | assertEquals(item, queryResult.getItems().get(0));
182 | }
183 |
184 | private void testQueryIsEmpty(final TransactionManagerDynamoDBFacade facade) {
185 | QueryRequest queryRequest = createQueryRequest(false);
186 | QueryResult queryResult = facade.query(queryRequest);
187 | assertNotNull(queryResult.getItems());
188 | assertEquals(0, queryResult.getItems().size());
189 | }
190 |
191 | private void testBatchGetItemsContainsItem(
192 | final TransactionManagerDynamoDBFacade facade,
193 | final Map item,
194 | final boolean filterAttributes) {
195 | BatchGetItemRequest batchGetItemRequest = createBatchGetItemRequest(filterAttributes);
196 | BatchGetItemResult batchGetItemResult = facade.batchGetItem(batchGetItemRequest);
197 | List