├── .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> items = batchGetItemResult.getResponses().get(INTEG_HASH_TABLE_NAME); 198 | assertEquals(1, items.size()); 199 | assertContainsNoTransactionAttributes(items.get(0)); 200 | assertEquals(item, items.get(0)); 201 | } 202 | 203 | private void testBatchGetItemsIsEmpty(final TransactionManagerDynamoDBFacade facade) { 204 | BatchGetItemRequest batchGetItemRequest = createBatchGetItemRequest(false); 205 | BatchGetItemResult batchGetItemResult = facade.batchGetItem(batchGetItemRequest); 206 | assertNotNull(batchGetItemResult.getResponses()); 207 | assertEquals(1, batchGetItemResult.getResponses().size()); 208 | assertNotNull(batchGetItemResult.getResponses().get(INTEG_HASH_TABLE_NAME)); 209 | assertEquals(0, batchGetItemResult.getResponses().get(INTEG_HASH_TABLE_NAME).size()); 210 | 211 | } 212 | 213 | /** 214 | * Test that calls to scan, query, getItem, and batchGetItems contain 215 | * the expected result. 216 | * @param facade The facade to test 217 | * @param item The expected item to be found 218 | * @param filterAttributes Whether or not to filter attributes using attributesToGet 219 | */ 220 | private void testReadCallsContainItem( 221 | final TransactionManagerDynamoDBFacade facade, 222 | final Map item, 223 | final boolean filterAttributes) { 224 | 225 | // GetItem contains the expected result 226 | testGetItemContainsItem(facade, item, filterAttributes); 227 | 228 | // Scan contains the expected result 229 | testScanContainsItem(facade, item, filterAttributes); 230 | 231 | // Query contains the expected result 232 | testQueryContainsItem(facade, item, filterAttributes); 233 | 234 | // BatchGetItems contains the expected result 235 | testBatchGetItemsContainsItem(facade, item, filterAttributes); 236 | } 237 | 238 | private void testReadCallsReturnEmpty(final TransactionManagerDynamoDBFacade facade) { 239 | 240 | // GetItem contains null 241 | testGetItemContainsItem(facade, null, false); 242 | 243 | // Scan returns empty 244 | testScanIsEmpty(facade); 245 | 246 | // Query returns empty 247 | testQueryIsEmpty(facade); 248 | 249 | // BatchGetItems does not return item 250 | testBatchGetItemsIsEmpty(facade); 251 | } 252 | 253 | @Test 254 | public void uncommittedFacadeReadsItemIfCommitted() { 255 | putItem(true); 256 | 257 | // test that read calls contain the committed item 258 | testReadCallsContainItem(uncommittedFacade, item0, false); 259 | 260 | // test that read calls contain the committed item respecting attributesToGet 261 | testReadCallsContainItem(uncommittedFacade, item0Filtered, true); 262 | } 263 | 264 | @Test 265 | public void uncommittedFacadeReadsItemIfNotCommitted() { 266 | putItem(false); 267 | 268 | // test that read calls contain the uncommitted item 269 | testReadCallsContainItem(uncommittedFacade, item0, false); 270 | 271 | // test that read calls contain the uncommitted item respecting attributesToGet 272 | testReadCallsContainItem(uncommittedFacade, item0Filtered, true); 273 | } 274 | 275 | @Test 276 | public void uncommittedFacadeReadsUncommittedUpdate() { 277 | putItem(true); 278 | updateItem(false); 279 | 280 | // test that read calls contain the updated uncommitted item 281 | testReadCallsContainItem(uncommittedFacade, item0Updated, false); 282 | } 283 | 284 | @Test 285 | public void committedFacadeReadsCommittedItem() { 286 | putItem(true); 287 | 288 | // test that read calls contain the committed item 289 | testReadCallsContainItem(committedFacade, item0, false); 290 | 291 | // test that read calls contain the committed item respecting attributesToGet 292 | testReadCallsContainItem(committedFacade, item0Filtered, true); 293 | } 294 | 295 | @Test 296 | public void committedFacadeDoesNotReadUncommittedItem() { 297 | putItem(false); 298 | 299 | // test that read calls do not contain the uncommitted item 300 | testReadCallsReturnEmpty(committedFacade); 301 | } 302 | 303 | @Test 304 | public void committedFacadeDoesNotReadUncommittedUpdate() { 305 | putItem(true); 306 | updateItem(false); 307 | 308 | // test that read calls contain the last committed version of the item 309 | testReadCallsContainItem(committedFacade, item0, false); 310 | 311 | // test that read calls contain the last committed version of the item 312 | // respecting attributesToGet 313 | testReadCallsContainItem(committedFacade, item0Filtered, true); 314 | } 315 | 316 | } 317 | -------------------------------------------------------------------------------- /integration/src/test/resources/com/amazonaws/services/dynamodbv2/transactions/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 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 4.0.0 6 | com.amazonaws.services.dynamodbv2 7 | amazon-dynamodb-transactions 8 | jar 9 | Amazon DynamoDB Transactions on the AWS SDK for Java 10 | 1.1.2 11 | Amazon DynamoDB Transactions on the AWS SDK for Java provide client-side multi-item, multi-table transactions on top of Amazon DynamoDB. 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 | 1.11.31 28 | 29 | 30 | 31 | 32 | 33 | com.amazonaws 34 | aws-java-sdk 35 | ${aws-java-sdk.version} 36 | 37 | 38 | 39 | 40 | junit 41 | junit 42 | 4.13.1 43 | true 44 | 45 | 46 | 47 | org.mockito 48 | mockito-core 49 | 1.9.5 50 | test 51 | 52 | 53 | 54 | 55 | 56 | amazonwebservices 57 | Amazon Web Services 58 | https://aws.amazon.com 59 | 60 | developer 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.apache.maven.plugins 70 | maven-compiler-plugin 71 | 72 | 1.6 73 | 1.6 74 | UTF-8 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/ReadCommittedIsolationHandlerImpl.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2016 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.services.dynamodbv2.model.AttributeValue; 8 | import com.amazonaws.services.dynamodbv2.model.GetItemRequest; 9 | import com.amazonaws.services.dynamodbv2.transactions.exceptions.TransactionException; 10 | import com.amazonaws.services.dynamodbv2.transactions.exceptions.TransactionNotFoundException; 11 | import com.amazonaws.services.dynamodbv2.transactions.exceptions.UnknownCompletedTransactionException; 12 | import org.apache.commons.logging.Log; 13 | import org.apache.commons.logging.LogFactory; 14 | 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | import static com.amazonaws.services.dynamodbv2.transactions.Transaction.getOwner; 20 | import static com.amazonaws.services.dynamodbv2.transactions.Transaction.isApplied; 21 | import static com.amazonaws.services.dynamodbv2.transactions.Transaction.isTransient; 22 | import static com.amazonaws.services.dynamodbv2.transactions.exceptions.TransactionAssertionException.txAssert; 23 | 24 | /** 25 | * An isolation handler for reading items at the committed 26 | * (Transaction.IsolationLevel.COMMITTED) level. It will 27 | * filter out transient items. If there is an applied, but 28 | * uncommitted item, the isolation handler will attempt to 29 | * get the last committed version of the item. 30 | */ 31 | public class ReadCommittedIsolationHandlerImpl implements ReadIsolationHandler { 32 | 33 | private static final int DEFAULT_NUM_RETRIES = 2; 34 | private static final Log LOG = LogFactory.getLog(ReadCommittedIsolationHandlerImpl.class); 35 | 36 | private final TransactionManager txManager; 37 | private final int numRetries; 38 | 39 | public ReadCommittedIsolationHandlerImpl(final TransactionManager txManager) { 40 | this(txManager, DEFAULT_NUM_RETRIES); 41 | } 42 | 43 | public ReadCommittedIsolationHandlerImpl(final TransactionManager txManager, final int numRetries) { 44 | this.txManager = txManager; 45 | this.numRetries = numRetries; 46 | } 47 | 48 | /** 49 | * Return the item that's passed in if it's not locked. Otherwise, throw a TransactionException. 50 | * @param item The item to check 51 | * @return The item if it's locked (or if it's locked, but not yet applied) 52 | */ 53 | protected Map checkItemCommitted(final Map item) { 54 | // If the item doesn't exist, it's not locked 55 | if (item == null) { 56 | return null; 57 | } 58 | // If the item is transient, return null 59 | if (isTransient(item)) { 60 | return null; 61 | } 62 | // If the item isn't applied, it doesn't matter if it's locked 63 | if (!isApplied(item)) { 64 | return item; 65 | } 66 | // If the item isn't locked, return it 67 | String lockingTxId = getOwner(item); 68 | if (lockingTxId == null) { 69 | return item; 70 | } 71 | 72 | throw new TransactionException(lockingTxId, "Item has been modified in an uncommitted transaction."); 73 | } 74 | 75 | /** 76 | * Get an old committed version of an item from the images table. 77 | * @param lockingTx The transaction that is currently locking the item. 78 | * @param tableName The table that contains the item 79 | * @param key The item's key 80 | * @return a previously committed version of the item 81 | */ 82 | protected Map getOldCommittedItem( 83 | final Transaction lockingTx, 84 | final String tableName, 85 | final Map key) { 86 | Request lockingRequest = lockingTx.getTxItem().getRequestForKey(tableName, key); 87 | txAssert(lockingRequest != null, null, "Expected transaction to be locking request, but no request found for tx", lockingTx.getId(), "table", tableName, "key ", key); 88 | Map oldItem = lockingTx.getTxItem().loadItemImage(lockingRequest.getRid()); 89 | if (oldItem == null) { 90 | if (LOG.isDebugEnabled()) { 91 | LOG.debug("Item image " + lockingRequest.getRid() + " missing for transaction " + lockingTx.getId()); 92 | } 93 | throw new UnknownCompletedTransactionException( 94 | lockingTx.getId(), 95 | "Transaction must have completed since the old copy of the image is missing"); 96 | } 97 | return oldItem; 98 | } 99 | 100 | /** 101 | * Create a GetItemRequest for an item (in the event that you need to get the item again). 102 | * @param tableName The table that holds the item 103 | * @param item The item to get 104 | * @return the request 105 | */ 106 | protected GetItemRequest createGetItemRequest( 107 | final String tableName, 108 | final Map item) { 109 | Map key = txManager.createKeyMap(tableName, item); 110 | 111 | /* 112 | * Set the request to consistent read the next time around, since we may have read while locking tx 113 | * was cleaning up or read a stale item that is no longer locked 114 | */ 115 | GetItemRequest request = new GetItemRequest() 116 | .withTableName(tableName) 117 | .withKey(key) 118 | .withConsistentRead(true); 119 | return request; 120 | } 121 | 122 | protected Transaction loadTransaction(String txId) { 123 | return new Transaction(txId, txManager, false); 124 | } 125 | 126 | /** 127 | * Returns the item that's passed in if it's not locked. Otherwise, tries to get an old 128 | * committed version of the item. If that's not possible, it retries. 129 | * @param item The item to check. 130 | * @param tableName The table that contains the item 131 | * @return A committed version of the item (not necessarily the latest committed version). 132 | */ 133 | protected Map handleItem( 134 | final Map item, 135 | final String tableName, 136 | final int numRetries) { 137 | GetItemRequest request = null; // only create if necessary 138 | for (int i = 0; i <= numRetries; i++) { 139 | final Map currentItem; 140 | if (i == 0) { 141 | currentItem = item; 142 | } else { 143 | if (request == null) { 144 | request = createGetItemRequest(tableName, item); 145 | } 146 | currentItem = txManager.getClient().getItem(request).getItem(); 147 | } 148 | 149 | // 1. Return the item if it isn't locked (or if it's locked, but not applied yet) 150 | try { 151 | return checkItemCommitted(currentItem); 152 | } catch (TransactionException e1) { 153 | try { 154 | // 2. Load the locking transaction 155 | Transaction lockingTx = loadTransaction(e1.getTxId()); 156 | 157 | /* 158 | * 3. See if the locking transaction has been committed. If so, return the item. This is valid because you cannot 159 | * write to an item multiple times in the same transaction. Otherwise it would expose intermediate state. 160 | */ 161 | if (TransactionItem.State.COMMITTED.equals(lockingTx.getTxItem().getState())) { 162 | return currentItem; 163 | } 164 | 165 | // 4. Try to get a previously committed version of the item 166 | if (request == null) { 167 | request = createGetItemRequest(tableName, item); 168 | } 169 | return getOldCommittedItem(lockingTx, tableName, request.getKey()); 170 | } catch (UnknownCompletedTransactionException e2) { 171 | LOG.debug("Could not find item image. Transaction must have already completed.", e2); 172 | } catch (TransactionNotFoundException e2) { 173 | LOG.debug("Unable to find locking transaction. Transaction must have already completed.", e2); 174 | } 175 | } 176 | } 177 | throw new TransactionException(null, "Ran out of attempts to get a committed image of the item"); 178 | } 179 | 180 | protected Map filterAttributesToGet( 181 | final Map item, 182 | final List attributesToGet) { 183 | if (item == null) { 184 | return null; 185 | } 186 | if (attributesToGet == null || attributesToGet.isEmpty()) { 187 | return item; 188 | } 189 | Map result = new HashMap(); 190 | for (String attributeName : attributesToGet) { 191 | AttributeValue value = item.get(attributeName); 192 | if (value != null) { 193 | result.put(attributeName, value); 194 | } 195 | } 196 | return result; 197 | } 198 | 199 | @Override 200 | public Map handleItem( 201 | final Map item, 202 | final List attributesToGet, 203 | final String tableName) { 204 | return filterAttributesToGet( 205 | handleItem(item, tableName, numRetries), 206 | attributesToGet); 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/ReadIsolationHandler.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2016 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 | 8 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | /** 14 | * An isolation handler takes an item and returns a version 15 | * of the item that can be read at the implemented isolaction 16 | * level. 17 | */ 18 | public interface ReadIsolationHandler { 19 | 20 | /** 21 | * Returns a version of the item can be read at the isolation level implemented by 22 | * the handler. This is possibly null if the item is transient. It might not be latest 23 | * version if the isolation level is committed. 24 | * @param item The item to check 25 | * @param attributesToGet The attributes to get from the table. If null or empty, will 26 | * fetch all attributes. 27 | * @param tableName The table that contains the item 28 | * @return A version of the item that can be read at the isolation level. 29 | */ 30 | public Map handleItem( 31 | final Map item, 32 | final List attributesToGet, 33 | final String tableName); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/ReadUncommittedIsolationHandlerImpl.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2016 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.services.dynamodbv2.model.AttributeValue; 8 | import org.apache.commons.logging.Log; 9 | import org.apache.commons.logging.LogFactory; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | import static com.amazonaws.services.dynamodbv2.transactions.Transaction.isApplied; 15 | import static com.amazonaws.services.dynamodbv2.transactions.Transaction.isTransient; 16 | 17 | /** 18 | * An isolation handler for reading items at the uncommitted 19 | * (Transaction.IsolationLevel.UNCOMMITTED) level. It will 20 | * only filter out transient items. 21 | */ 22 | public class ReadUncommittedIsolationHandlerImpl implements ReadIsolationHandler { 23 | 24 | private static final Log LOG = LogFactory.getLog(ReadUncommittedIsolationHandlerImpl.class); 25 | 26 | /** 27 | * Given an item, return whatever is there. The returned item may contain changes that will later be rolled back. 28 | * If the item was inserted only for acquiring a lock (and the item will be gone after the transaction), the returned 29 | * item will be null. 30 | * @param item The item that the client read. 31 | * @param attributesToGet The attributes to get from the table. If null or empty, will 32 | * fetch all attributes. 33 | * @param tableName the table that contains the item 34 | * @return the item itself, unless it is transient and not applied. 35 | */ 36 | @Override 37 | public Map handleItem( 38 | final Map item, 39 | final List attributesToGet, 40 | final String tableName) { 41 | // If the item doesn't exist, it's not locked 42 | if (item == null) { 43 | return null; 44 | } 45 | 46 | // If the item is transient, return a null item 47 | // But if the change is applied, return it even if it was a transient item (delete and lock do not apply) 48 | if (isTransient(item) && !isApplied(item)) { 49 | return null; 50 | } 51 | return item; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/ThreadLocalDynamoDBFacade.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014-2014 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.List; 8 | import java.util.Map; 9 | 10 | import com.amazonaws.AmazonClientException; 11 | import com.amazonaws.AmazonServiceException; 12 | import com.amazonaws.AmazonWebServiceRequest; 13 | import com.amazonaws.ResponseMetadata; 14 | import com.amazonaws.regions.Region; 15 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; 16 | import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; 17 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 18 | import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; 19 | import com.amazonaws.services.dynamodbv2.model.BatchGetItemRequest; 20 | import com.amazonaws.services.dynamodbv2.model.BatchGetItemResult; 21 | import com.amazonaws.services.dynamodbv2.model.BatchWriteItemRequest; 22 | import com.amazonaws.services.dynamodbv2.model.BatchWriteItemResult; 23 | import com.amazonaws.services.dynamodbv2.model.Condition; 24 | import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; 25 | import com.amazonaws.services.dynamodbv2.model.CreateTableResult; 26 | import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; 27 | import com.amazonaws.services.dynamodbv2.model.DeleteItemResult; 28 | import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; 29 | import com.amazonaws.services.dynamodbv2.model.DeleteTableResult; 30 | import com.amazonaws.services.dynamodbv2.model.DescribeLimitsRequest; 31 | import com.amazonaws.services.dynamodbv2.model.DescribeLimitsResult; 32 | import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; 33 | import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; 34 | import com.amazonaws.services.dynamodbv2.model.GetItemRequest; 35 | import com.amazonaws.services.dynamodbv2.model.GetItemResult; 36 | import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; 37 | import com.amazonaws.services.dynamodbv2.model.KeysAndAttributes; 38 | import com.amazonaws.services.dynamodbv2.model.ListTablesRequest; 39 | import com.amazonaws.services.dynamodbv2.model.ListTablesResult; 40 | import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; 41 | import com.amazonaws.services.dynamodbv2.model.PutItemRequest; 42 | import com.amazonaws.services.dynamodbv2.model.PutItemResult; 43 | import com.amazonaws.services.dynamodbv2.model.QueryRequest; 44 | import com.amazonaws.services.dynamodbv2.model.QueryResult; 45 | import com.amazonaws.services.dynamodbv2.model.ScanRequest; 46 | import com.amazonaws.services.dynamodbv2.model.ScanResult; 47 | import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest; 48 | import com.amazonaws.services.dynamodbv2.model.UpdateItemResult; 49 | import com.amazonaws.services.dynamodbv2.model.UpdateTableRequest; 50 | import com.amazonaws.services.dynamodbv2.model.UpdateTableResult; 51 | import com.amazonaws.services.dynamodbv2.model.WriteRequest; 52 | import com.amazonaws.services.dynamodbv2.waiters.AmazonDynamoDBWaiters; 53 | 54 | /** 55 | * Necessary to work around a limitation of the mapper. The mapper always gets 56 | * created with a fresh reflection cache, which is expensive to repopulate. 57 | * Using this class to route to a different facade for each request allows us to 58 | * reuse the mapper and its underlying cache for each call to the mapper from a 59 | * transaction or the transaction manager. 60 | */ 61 | public class ThreadLocalDynamoDBFacade implements AmazonDynamoDB { 62 | 63 | private final ThreadLocal backend = new ThreadLocal(); 64 | 65 | private AmazonDynamoDB getBackend() { 66 | if (backend.get() == null) { 67 | throw new RuntimeException("No backend to proxy"); 68 | } 69 | return backend.get(); 70 | } 71 | 72 | public void setBackend(AmazonDynamoDB newBackend) { 73 | backend.set(newBackend); 74 | } 75 | 76 | @Override 77 | public BatchGetItemResult batchGetItem(BatchGetItemRequest request) throws AmazonServiceException, AmazonClientException { 78 | return getBackend().batchGetItem(request); 79 | } 80 | 81 | @Override 82 | public BatchWriteItemResult batchWriteItem(BatchWriteItemRequest request) throws AmazonServiceException, AmazonClientException { 83 | return getBackend().batchWriteItem(request); 84 | } 85 | 86 | @Override 87 | public CreateTableResult createTable(CreateTableRequest request) throws AmazonServiceException, AmazonClientException { 88 | return getBackend().createTable(request); 89 | } 90 | 91 | @Override 92 | public DeleteItemResult deleteItem(DeleteItemRequest request) throws AmazonServiceException, AmazonClientException { 93 | return getBackend().deleteItem(request); 94 | } 95 | 96 | @Override 97 | public DeleteTableResult deleteTable(DeleteTableRequest request) throws AmazonServiceException, AmazonClientException { 98 | return getBackend().deleteTable(request); 99 | } 100 | 101 | @Override 102 | public DescribeTableResult describeTable(DescribeTableRequest request) throws AmazonServiceException, AmazonClientException { 103 | return getBackend().describeTable(request); 104 | } 105 | 106 | @Override 107 | public ResponseMetadata getCachedResponseMetadata(AmazonWebServiceRequest request) { 108 | return getBackend().getCachedResponseMetadata(request); 109 | } 110 | 111 | @Override 112 | public GetItemResult getItem(GetItemRequest request) throws AmazonServiceException, AmazonClientException { 113 | return getBackend().getItem(request); 114 | } 115 | 116 | @Override 117 | public ListTablesResult listTables() throws AmazonServiceException, AmazonClientException { 118 | return getBackend().listTables(); 119 | } 120 | 121 | @Override 122 | public ListTablesResult listTables(ListTablesRequest request) throws AmazonServiceException, AmazonClientException { 123 | return getBackend().listTables(request); 124 | } 125 | 126 | @Override 127 | public PutItemResult putItem(PutItemRequest request) throws AmazonServiceException, AmazonClientException { 128 | return getBackend().putItem(request); 129 | } 130 | 131 | @Override 132 | public QueryResult query(QueryRequest request) throws AmazonServiceException, AmazonClientException { 133 | return getBackend().query(request); 134 | } 135 | 136 | @Override 137 | public ScanResult scan(ScanRequest request) throws AmazonServiceException, AmazonClientException { 138 | return getBackend().scan(request); 139 | } 140 | 141 | @Override 142 | public void setEndpoint(String request) throws IllegalArgumentException { 143 | getBackend().setEndpoint(request); 144 | } 145 | 146 | @Override 147 | public void setRegion(Region request) throws IllegalArgumentException { 148 | getBackend().setRegion(request); 149 | } 150 | 151 | @Override 152 | public void shutdown() { 153 | getBackend().shutdown(); 154 | } 155 | 156 | @Override 157 | public UpdateItemResult updateItem(UpdateItemRequest request) throws AmazonServiceException, AmazonClientException { 158 | return getBackend().updateItem(request); 159 | } 160 | 161 | @Override 162 | public UpdateTableResult updateTable(UpdateTableRequest request) throws AmazonServiceException, AmazonClientException { 163 | return getBackend().updateTable(request); 164 | } 165 | 166 | @Override 167 | public ScanResult scan(String tableName, List attributesToGet) throws AmazonServiceException, AmazonClientException { 168 | return getBackend().scan(tableName, attributesToGet); 169 | } 170 | 171 | @Override 172 | public ScanResult scan(String tableName, Map scanFilter) throws AmazonServiceException, AmazonClientException { 173 | return getBackend().scan(tableName, scanFilter); 174 | } 175 | 176 | @Override 177 | public ScanResult scan(String tableName, List attributesToGet, Map scanFilter) throws AmazonServiceException, AmazonClientException { 178 | return getBackend().scan(tableName, attributesToGet, scanFilter); 179 | } 180 | 181 | @Override 182 | public UpdateTableResult updateTable(String tableName, ProvisionedThroughput provisionedThroughput) throws AmazonServiceException, AmazonClientException { 183 | return getBackend().updateTable(tableName, provisionedThroughput); 184 | } 185 | 186 | @Override 187 | public DeleteTableResult deleteTable(String tableName) throws AmazonServiceException, AmazonClientException { 188 | return getBackend().deleteTable(tableName); 189 | } 190 | 191 | @Override 192 | public BatchWriteItemResult batchWriteItem(Map> requestItems) throws AmazonServiceException, AmazonClientException { 193 | return getBackend().batchWriteItem(requestItems); 194 | } 195 | 196 | @Override 197 | public DescribeTableResult describeTable(String tableName) throws AmazonServiceException, AmazonClientException { 198 | return getBackend().describeTable(tableName); 199 | } 200 | 201 | @Override 202 | public GetItemResult getItem(String tableName, Map key) throws AmazonServiceException, AmazonClientException { 203 | return getBackend().getItem(tableName, key); 204 | } 205 | 206 | @Override 207 | public GetItemResult getItem(String tableName, Map key, Boolean consistentRead) throws AmazonServiceException, AmazonClientException { 208 | return getBackend().getItem(tableName, key, consistentRead); 209 | } 210 | 211 | @Override 212 | public DeleteItemResult deleteItem(String tableName, Map key) throws AmazonServiceException, AmazonClientException { 213 | return getBackend().deleteItem(tableName, key); 214 | } 215 | 216 | @Override 217 | public DeleteItemResult deleteItem(String tableName, Map key, String returnValues) throws AmazonServiceException, AmazonClientException { 218 | return getBackend().deleteItem(tableName, key, returnValues); 219 | } 220 | 221 | @Override 222 | public CreateTableResult createTable( 223 | List attributeDefinitions, String tableName, 224 | List keySchema, 225 | ProvisionedThroughput provisionedThroughput) throws AmazonServiceException, AmazonClientException { 226 | return getBackend().createTable(attributeDefinitions, tableName, keySchema, provisionedThroughput); 227 | } 228 | 229 | @Override 230 | public PutItemResult putItem(String tableName, Map item) throws AmazonServiceException, AmazonClientException { 231 | return getBackend().putItem(tableName, item); 232 | } 233 | 234 | @Override 235 | public PutItemResult putItem(String tableName, Map item, String returnValues) throws AmazonServiceException, AmazonClientException { 236 | return getBackend().putItem(tableName, item, returnValues); 237 | } 238 | 239 | @Override 240 | public ListTablesResult listTables(String exclusiveStartTableName) throws AmazonServiceException, AmazonClientException { 241 | return getBackend().listTables(exclusiveStartTableName); 242 | } 243 | 244 | @Override 245 | public ListTablesResult listTables(String exclusiveStartTableName, Integer limit) throws AmazonServiceException, AmazonClientException { 246 | return getBackend().listTables(exclusiveStartTableName, limit); 247 | } 248 | 249 | @Override 250 | public ListTablesResult listTables(Integer limit) throws AmazonServiceException, AmazonClientException { 251 | return getBackend().listTables(limit); 252 | } 253 | 254 | @Override 255 | public UpdateItemResult updateItem(String tableName, 256 | Map key, 257 | Map attributeUpdates) throws AmazonServiceException, AmazonClientException { 258 | return getBackend().updateItem(tableName, key, attributeUpdates); 259 | } 260 | 261 | @Override 262 | public UpdateItemResult updateItem(String tableName, Map key, Map attributeUpdates, String returnValues) throws AmazonServiceException, AmazonClientException { 263 | return getBackend().updateItem(tableName, key, attributeUpdates, returnValues); 264 | } 265 | 266 | @Override 267 | public BatchGetItemResult batchGetItem(Map requestItems, String returnConsumedCapacity) throws AmazonServiceException, AmazonClientException { 268 | return getBackend().batchGetItem(requestItems, returnConsumedCapacity); 269 | } 270 | 271 | @Override 272 | public BatchGetItemResult batchGetItem(Map requestItems) throws AmazonServiceException, AmazonClientException { 273 | return getBackend().batchGetItem(requestItems); 274 | } 275 | 276 | @Override 277 | public DescribeLimitsResult describeLimits(DescribeLimitsRequest request) { 278 | return getBackend().describeLimits(request); 279 | } 280 | 281 | @Override 282 | public AmazonDynamoDBWaiters waiters() { 283 | return getBackend().waiters(); 284 | } 285 | 286 | } 287 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/TransactionDynamoDBFacade.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014 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.math.BigDecimal; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | import com.amazonaws.AmazonClientException; 12 | import com.amazonaws.AmazonServiceException; 13 | import com.amazonaws.AmazonWebServiceRequest; 14 | import com.amazonaws.ResponseMetadata; 15 | import com.amazonaws.regions.Region; 16 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; 17 | import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; 18 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 19 | import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; 20 | import com.amazonaws.services.dynamodbv2.model.BatchGetItemRequest; 21 | import com.amazonaws.services.dynamodbv2.model.BatchGetItemResult; 22 | import com.amazonaws.services.dynamodbv2.model.BatchWriteItemRequest; 23 | import com.amazonaws.services.dynamodbv2.model.BatchWriteItemResult; 24 | import com.amazonaws.services.dynamodbv2.model.Condition; 25 | import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException; 26 | import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; 27 | import com.amazonaws.services.dynamodbv2.model.CreateTableResult; 28 | import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; 29 | import com.amazonaws.services.dynamodbv2.model.DeleteItemResult; 30 | import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; 31 | import com.amazonaws.services.dynamodbv2.model.DeleteTableResult; 32 | import com.amazonaws.services.dynamodbv2.model.DescribeLimitsRequest; 33 | import com.amazonaws.services.dynamodbv2.model.DescribeLimitsResult; 34 | import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; 35 | import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; 36 | import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue; 37 | import com.amazonaws.services.dynamodbv2.model.GetItemRequest; 38 | import com.amazonaws.services.dynamodbv2.model.GetItemResult; 39 | import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; 40 | import com.amazonaws.services.dynamodbv2.model.KeysAndAttributes; 41 | import com.amazonaws.services.dynamodbv2.model.ListTablesRequest; 42 | import com.amazonaws.services.dynamodbv2.model.ListTablesResult; 43 | import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; 44 | import com.amazonaws.services.dynamodbv2.model.PutItemRequest; 45 | import com.amazonaws.services.dynamodbv2.model.PutItemResult; 46 | import com.amazonaws.services.dynamodbv2.model.QueryRequest; 47 | import com.amazonaws.services.dynamodbv2.model.QueryResult; 48 | import com.amazonaws.services.dynamodbv2.model.ScanRequest; 49 | import com.amazonaws.services.dynamodbv2.model.ScanResult; 50 | import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest; 51 | import com.amazonaws.services.dynamodbv2.model.UpdateItemResult; 52 | import com.amazonaws.services.dynamodbv2.model.UpdateTableRequest; 53 | import com.amazonaws.services.dynamodbv2.model.UpdateTableResult; 54 | import com.amazonaws.services.dynamodbv2.model.WriteRequest; 55 | import com.amazonaws.services.dynamodbv2.waiters.AmazonDynamoDBWaiters; 56 | 57 | /** 58 | * Facade for {@link AmazonDynamoDB} that forwards requests to a 59 | * {@link Transaction}, omitting conditional checks and consistent read options 60 | * from each request. Only supports the operations needed by DynamoDBMapper for 61 | * loading, saving or deleting items. 62 | */ 63 | public class TransactionDynamoDBFacade implements AmazonDynamoDB { 64 | 65 | private final Transaction txn; 66 | private final TransactionManager txManager; 67 | 68 | public TransactionDynamoDBFacade(Transaction txn, TransactionManager txManager) { 69 | this.txn = txn; 70 | this.txManager = txManager; 71 | } 72 | 73 | @Override 74 | public DeleteItemResult deleteItem(DeleteItemRequest request) 75 | throws AmazonServiceException, AmazonClientException { 76 | Map expectedValues = request.getExpected(); 77 | checkExpectedValues(request.getTableName(), request.getKey(), expectedValues); 78 | 79 | // conditional checks are handled by the above call 80 | request.setExpected(null); 81 | return txn.deleteItem(request); 82 | } 83 | 84 | @Override 85 | public GetItemResult getItem(GetItemRequest request) 86 | throws AmazonServiceException, AmazonClientException { 87 | return txn.getItem(request); 88 | } 89 | 90 | @Override 91 | public PutItemResult putItem(PutItemRequest request) 92 | throws AmazonServiceException, AmazonClientException { 93 | Map expectedValues = request.getExpected(); 94 | checkExpectedValues(request.getTableName(), Request.getKeyFromItem(request.getTableName(), 95 | request.getItem(), txManager), expectedValues); 96 | 97 | // conditional checks are handled by the above call 98 | request.setExpected(null); 99 | return txn.putItem(request); 100 | } 101 | 102 | @Override 103 | public UpdateItemResult updateItem(UpdateItemRequest request) 104 | throws AmazonServiceException, AmazonClientException { 105 | Map expectedValues = request.getExpected(); 106 | checkExpectedValues(request.getTableName(), request.getKey(), expectedValues); 107 | 108 | // conditional checks are handled by the above call 109 | request.setExpected(null); 110 | return txn.updateItem(request); 111 | } 112 | 113 | private void checkExpectedValues(String tableName, 114 | Map itemKey, 115 | Map expectedValues) { 116 | if (expectedValues != null && !expectedValues.isEmpty()) { 117 | for (Map.Entry entry : expectedValues.entrySet()) { 118 | if ((entry.getValue().isExists() == null || entry.getValue().isExists() == true) 119 | && entry.getValue().getValue() == null) { 120 | throw new IllegalArgumentException("An explicit value is required when Exists is null or true, " 121 | + "but none was found in expected values for item with key " + itemKey + 122 | ": " + expectedValues); 123 | } 124 | } 125 | 126 | // simulate by loading the item and checking the values; 127 | // this also has the effect of locking the item, which gives the 128 | // same behavior 129 | GetItemResult result = getItem(new GetItemRequest() 130 | .withAttributesToGet(expectedValues.keySet()) 131 | .withKey(itemKey) 132 | .withTableName(tableName)); 133 | Map item = result.getItem(); 134 | try { 135 | checkExpectedValues(expectedValues, item); 136 | } catch (ConditionalCheckFailedException e) { 137 | throw new ConditionalCheckFailedException("Item " + itemKey + " had unexpected attributes: " + e.getMessage()); 138 | } 139 | } 140 | } 141 | 142 | /** 143 | * Checks a map of expected values against a map of actual values in a way 144 | * that's compatible with how the DynamoDB service interprets the Expected 145 | * parameter of PutItem, UpdateItem and DeleteItem. 146 | * 147 | * @param expectedValues 148 | * A description of the expected values. 149 | * @param item 150 | * The actual values. 151 | * @throws ConditionalCheckFailedException 152 | * Thrown if the values do not match the expected values. 153 | */ 154 | public static void checkExpectedValues(Map expectedValues, Map item) { 155 | for (Map.Entry entry : expectedValues.entrySet()) { 156 | // if the attribute is expected to exist (null for isExists means 157 | // true) 158 | if ((entry.getValue().isExists() == null || entry.getValue().isExists() == true) 159 | // but the item doesn't 160 | && (item == null 161 | // or the attribute doesn't 162 | || item.get(entry.getKey()) == null 163 | // or it doesn't have the expected value 164 | || !expectedValueMatches(entry.getValue().getValue(), item.get(entry.getKey())))) { 165 | throw new ConditionalCheckFailedException( 166 | "expected attribute(s) " + expectedValues 167 | + " but found " + item); 168 | } else if (entry.getValue().isExists() != null 169 | && !entry.getValue().isExists() 170 | && item != null && item.get(entry.getKey()) != null) { 171 | // the attribute isn't expected to exist, but the item exists 172 | // and the attribute does too 173 | throw new ConditionalCheckFailedException( 174 | "expected attribute(s) " + expectedValues 175 | + " but found " + item); 176 | } 177 | } 178 | } 179 | 180 | private static boolean expectedValueMatches(AttributeValue expected, AttributeValue actual) { 181 | if (expected.getN() != null) { 182 | return actual.getN() != null && new BigDecimal(expected.getN()).compareTo(new BigDecimal(actual.getN())) == 0; 183 | } else if (expected.getS() != null || expected.getB() != null) { 184 | return expected.equals(actual); 185 | } else { 186 | throw new IllegalArgumentException("Expect condition using unsupported value type: " + expected); 187 | } 188 | } 189 | 190 | @Override 191 | public BatchGetItemResult batchGetItem(BatchGetItemRequest arg0) 192 | throws AmazonServiceException, AmazonClientException { 193 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 194 | } 195 | 196 | @Override 197 | public BatchWriteItemResult batchWriteItem(BatchWriteItemRequest arg0) 198 | throws AmazonServiceException, AmazonClientException { 199 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 200 | } 201 | 202 | @Override 203 | public CreateTableResult createTable(CreateTableRequest arg0) 204 | throws AmazonServiceException, AmazonClientException { 205 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 206 | } 207 | 208 | @Override 209 | public DeleteTableResult deleteTable(DeleteTableRequest arg0) 210 | throws AmazonServiceException, AmazonClientException { 211 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 212 | } 213 | 214 | @Override 215 | public DescribeTableResult describeTable(DescribeTableRequest arg0) 216 | throws AmazonServiceException, AmazonClientException { 217 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 218 | } 219 | 220 | @Override 221 | public ResponseMetadata getCachedResponseMetadata( 222 | AmazonWebServiceRequest arg0) { 223 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 224 | } 225 | 226 | @Override 227 | public ListTablesResult listTables() throws AmazonServiceException, 228 | AmazonClientException { 229 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 230 | } 231 | 232 | @Override 233 | public ListTablesResult listTables(ListTablesRequest arg0) 234 | throws AmazonServiceException, AmazonClientException { 235 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 236 | } 237 | 238 | @Override 239 | public QueryResult query(QueryRequest arg0) throws AmazonServiceException, 240 | AmazonClientException { 241 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 242 | } 243 | 244 | @Override 245 | public ScanResult scan(ScanRequest arg0) throws AmazonServiceException, 246 | AmazonClientException { 247 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 248 | } 249 | 250 | @Override 251 | public void setEndpoint(String arg0) throws IllegalArgumentException { 252 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 253 | } 254 | 255 | @Override 256 | public void setRegion(Region arg0) throws IllegalArgumentException { 257 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 258 | } 259 | 260 | @Override 261 | public void shutdown() { 262 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 263 | } 264 | 265 | @Override 266 | public UpdateTableResult updateTable(UpdateTableRequest arg0) 267 | throws AmazonServiceException, AmazonClientException { 268 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 269 | } 270 | 271 | @Override 272 | public ScanResult scan(String tableName, List attributesToGet) 273 | throws AmazonServiceException, AmazonClientException { 274 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 275 | } 276 | 277 | @Override 278 | public ScanResult scan(String tableName, Map scanFilter) 279 | throws AmazonServiceException, AmazonClientException { 280 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 281 | } 282 | 283 | @Override 284 | public ScanResult scan(String tableName, List attributesToGet, 285 | Map scanFilter) throws AmazonServiceException, 286 | AmazonClientException { 287 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 288 | } 289 | 290 | @Override 291 | public UpdateTableResult updateTable(String tableName, 292 | ProvisionedThroughput provisionedThroughput) 293 | throws AmazonServiceException, AmazonClientException { 294 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 295 | } 296 | 297 | @Override 298 | public DeleteTableResult deleteTable(String tableName) 299 | throws AmazonServiceException, AmazonClientException { 300 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 301 | } 302 | 303 | @Override 304 | public BatchWriteItemResult batchWriteItem( 305 | Map> requestItems) 306 | throws AmazonServiceException, AmazonClientException { 307 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 308 | } 309 | 310 | @Override 311 | public DescribeTableResult describeTable(String tableName) 312 | throws AmazonServiceException, AmazonClientException { 313 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 314 | } 315 | 316 | @Override 317 | public GetItemResult getItem(String tableName, 318 | Map key) throws AmazonServiceException, 319 | AmazonClientException { 320 | return getItem(new GetItemRequest() 321 | .withTableName(tableName) 322 | .withKey(key)); 323 | } 324 | 325 | @Override 326 | public GetItemResult getItem(String tableName, 327 | Map key, Boolean consistentRead) 328 | throws AmazonServiceException, AmazonClientException { 329 | return getItem(new GetItemRequest() 330 | .withTableName(tableName) 331 | .withKey(key) 332 | .withConsistentRead(consistentRead)); 333 | } 334 | 335 | @Override 336 | public DeleteItemResult deleteItem(String tableName, 337 | Map key) throws AmazonServiceException, 338 | AmazonClientException { 339 | return deleteItem(new DeleteItemRequest() 340 | .withTableName(tableName) 341 | .withKey(key)); 342 | } 343 | 344 | @Override 345 | public DeleteItemResult deleteItem(String tableName, 346 | Map key, String returnValues) 347 | throws AmazonServiceException, AmazonClientException { 348 | return deleteItem(new DeleteItemRequest() 349 | .withTableName(tableName) 350 | .withKey(key) 351 | .withReturnValues(returnValues)); 352 | } 353 | 354 | @Override 355 | public CreateTableResult createTable( 356 | List attributeDefinitions, String tableName, 357 | List keySchema, 358 | ProvisionedThroughput provisionedThroughput) 359 | throws AmazonServiceException, AmazonClientException { 360 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 361 | } 362 | 363 | @Override 364 | public PutItemResult putItem(String tableName, 365 | Map item) throws AmazonServiceException, 366 | AmazonClientException { 367 | return putItem(new PutItemRequest() 368 | .withTableName(tableName) 369 | .withItem(item)); 370 | } 371 | 372 | @Override 373 | public PutItemResult putItem(String tableName, 374 | Map item, String returnValues) 375 | throws AmazonServiceException, AmazonClientException { 376 | return putItem(new PutItemRequest() 377 | .withTableName(tableName) 378 | .withItem(item) 379 | .withReturnValues(returnValues)); 380 | } 381 | 382 | @Override 383 | public ListTablesResult listTables(String exclusiveStartTableName) 384 | throws AmazonServiceException, AmazonClientException { 385 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 386 | } 387 | 388 | @Override 389 | public ListTablesResult listTables(String exclusiveStartTableName, 390 | Integer limit) throws AmazonServiceException, AmazonClientException { 391 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 392 | } 393 | 394 | @Override 395 | public ListTablesResult listTables(Integer limit) 396 | throws AmazonServiceException, AmazonClientException { 397 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 398 | } 399 | 400 | @Override 401 | public UpdateItemResult updateItem(String tableName, 402 | Map key, 403 | Map attributeUpdates) 404 | throws AmazonServiceException, AmazonClientException { 405 | return updateItem(new UpdateItemRequest() 406 | .withTableName(tableName) 407 | .withKey(key) 408 | .withAttributeUpdates(attributeUpdates)); 409 | } 410 | 411 | @Override 412 | public UpdateItemResult updateItem(String tableName, 413 | Map key, 414 | Map attributeUpdates, 415 | String returnValues) throws AmazonServiceException, 416 | AmazonClientException { 417 | return updateItem(new UpdateItemRequest() 418 | .withTableName(tableName) 419 | .withKey(key) 420 | .withAttributeUpdates(attributeUpdates) 421 | .withReturnValues(returnValues)); 422 | } 423 | 424 | @Override 425 | public BatchGetItemResult batchGetItem( 426 | Map requestItems, 427 | String returnConsumedCapacity) throws AmazonServiceException, 428 | AmazonClientException { 429 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 430 | } 431 | 432 | @Override 433 | public BatchGetItemResult batchGetItem( 434 | Map requestItems) 435 | throws AmazonServiceException, AmazonClientException { 436 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 437 | } 438 | 439 | @Override 440 | public DescribeLimitsResult describeLimits(DescribeLimitsRequest request) { 441 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 442 | } 443 | 444 | @Override 445 | public AmazonDynamoDBWaiters waiters() { 446 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 447 | } 448 | 449 | } 450 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/TransactionManager.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014 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.services.dynamodbv2.AmazonDynamoDB; 8 | import com.amazonaws.services.dynamodbv2.datamodeling.AttributeTransformer; 9 | import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; 10 | import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig; 11 | import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; 12 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 13 | import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; 14 | import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; 15 | import com.amazonaws.services.dynamodbv2.model.GetItemRequest; 16 | import com.amazonaws.services.dynamodbv2.model.GetItemResult; 17 | import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; 18 | import com.amazonaws.services.dynamodbv2.model.KeyType; 19 | import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; 20 | import com.amazonaws.services.dynamodbv2.model.ResourceInUseException; 21 | import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; 22 | import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; 23 | import com.amazonaws.services.dynamodbv2.transactions.Transaction.AttributeName; 24 | import com.amazonaws.services.dynamodbv2.transactions.Transaction.IsolationLevel; 25 | import com.amazonaws.services.dynamodbv2.util.TableHelper; 26 | import org.apache.commons.logging.Log; 27 | import org.apache.commons.logging.LogFactory; 28 | 29 | import java.util.Arrays; 30 | import java.util.Collections; 31 | import java.util.Comparator; 32 | import java.util.HashMap; 33 | import java.util.HashSet; 34 | import java.util.List; 35 | import java.util.Map; 36 | import java.util.Set; 37 | import java.util.UUID; 38 | import java.util.concurrent.ConcurrentHashMap; 39 | 40 | /** 41 | * A factory for client-side transactions on DynamoDB. Thread-safe. 42 | */ 43 | public class TransactionManager { 44 | 45 | private static final Log log = LogFactory.getLog(TransactionManager.class); 46 | private static final List TRANSACTIONS_TABLE_ATTRIBUTES; 47 | private static final List TRANSACTIONS_TABLE_KEY_SCHEMA = Collections.unmodifiableList( 48 | Arrays.asList( 49 | new KeySchemaElement().withAttributeName(AttributeName.TXID.toString()).withKeyType(KeyType.HASH))); 50 | 51 | private static final List TRANSACTION_IMAGES_TABLE_ATTRIBUTES; 52 | private static final List TRANSACTION_IMAGES_TABLE_KEY_SCHEMA = Collections.unmodifiableList( 53 | Arrays.asList( 54 | new KeySchemaElement().withAttributeName(AttributeName.IMAGE_ID.toString()).withKeyType(KeyType.HASH))); 55 | 56 | static { 57 | List definition = Arrays.asList( 58 | new AttributeDefinition().withAttributeName(AttributeName.TXID.toString()).withAttributeType(ScalarAttributeType.S)); 59 | Collections.sort(definition, new AttributeDefinitionComparator()); 60 | TRANSACTIONS_TABLE_ATTRIBUTES = Collections.unmodifiableList(definition); 61 | 62 | definition = Arrays.asList( 63 | new AttributeDefinition().withAttributeName(AttributeName.IMAGE_ID.toString()).withAttributeType(ScalarAttributeType.S)); 64 | Collections.sort(definition, new AttributeDefinitionComparator()); 65 | TRANSACTION_IMAGES_TABLE_ATTRIBUTES = Collections.unmodifiableList(definition); 66 | } 67 | 68 | private final AmazonDynamoDB client; 69 | private final String transactionTableName; 70 | private final String itemImageTableName; 71 | private final ConcurrentHashMap> tableSchemaCache = new ConcurrentHashMap>(); 72 | private final DynamoDBMapper clientMapper; 73 | private final ThreadLocalDynamoDBFacade facadeProxy; 74 | private final ReadUncommittedIsolationHandlerImpl readUncommittedIsolationHandler; 75 | private final ReadCommittedIsolationHandlerImpl readCommittedIsolationHandler; 76 | 77 | public TransactionManager(AmazonDynamoDB client, String transactionTableName, String itemImageTableName) { 78 | this(client, transactionTableName, itemImageTableName, DynamoDBMapperConfig.DEFAULT); 79 | } 80 | 81 | public TransactionManager(AmazonDynamoDB client, String transactionTableName, String itemImageTableName, DynamoDBMapperConfig config) { 82 | this(client, transactionTableName, itemImageTableName, config, null); 83 | } 84 | 85 | public TransactionManager(AmazonDynamoDB client, String transactionTableName, String itemImageTableName, DynamoDBMapperConfig config, AttributeTransformer transformer) { 86 | if(client == null) { 87 | throw new IllegalArgumentException("client must not be null"); 88 | } 89 | if(transactionTableName == null) { 90 | throw new IllegalArgumentException("transactionTableName must not be null"); 91 | } 92 | if(itemImageTableName == null) { 93 | throw new IllegalArgumentException("itemImageTableName must not be null"); 94 | } 95 | this.client = client; 96 | this.transactionTableName = transactionTableName; 97 | this.itemImageTableName = itemImageTableName; 98 | this.facadeProxy = new ThreadLocalDynamoDBFacade(); 99 | this.clientMapper = new DynamoDBMapper(facadeProxy, config, transformer); 100 | this.readUncommittedIsolationHandler = new ReadUncommittedIsolationHandlerImpl(); 101 | this.readCommittedIsolationHandler = new ReadCommittedIsolationHandlerImpl(this); 102 | } 103 | 104 | protected List getTableSchema(String tableName) throws ResourceNotFoundException { 105 | List schema = tableSchemaCache.get(tableName); 106 | if(schema == null) { 107 | DescribeTableResult result = client.describeTable(new DescribeTableRequest().withTableName(tableName)); 108 | schema = Collections.unmodifiableList(result.getTable().getKeySchema()); 109 | tableSchemaCache.put(tableName, schema); 110 | } 111 | return schema; 112 | } 113 | 114 | protected Map createKeyMap(final String tableName, final Map item) { 115 | if (tableName == null) { 116 | throw new IllegalArgumentException("must specify a tableName"); 117 | } 118 | if (item == null) { 119 | throw new IllegalArgumentException("must specify an item"); 120 | } 121 | List schema = getTableSchema(tableName); 122 | Map key = new HashMap(schema.size()); 123 | for (KeySchemaElement element : schema) { 124 | key.put(element.getAttributeName(), item.get(element.getAttributeName())); 125 | } 126 | return key; 127 | } 128 | 129 | public Transaction newTransaction() { 130 | Transaction transaction = new Transaction(UUID.randomUUID().toString(), this, true); 131 | log.info("Started transaction " + transaction.getId()); 132 | return transaction; 133 | } 134 | 135 | public Transaction resumeTransaction(String txId) { 136 | Transaction transaction = new Transaction(txId, this, false); 137 | log.info("Resuming transaction from id " + transaction.getId()); 138 | return transaction; 139 | } 140 | 141 | public Transaction resumeTransaction(Map txItem) { 142 | Transaction transaction = new Transaction(txItem, this); 143 | log.info("Resuming transaction from item " + transaction.getId()); 144 | return transaction; 145 | } 146 | 147 | public static boolean isTransactionItem(Map txItem) { 148 | return TransactionItem.isTransactionItem(txItem); 149 | } 150 | 151 | public AmazonDynamoDB getClient() { 152 | return client; 153 | } 154 | 155 | public DynamoDBMapper getClientMapper() { 156 | return clientMapper; 157 | } 158 | 159 | protected ThreadLocalDynamoDBFacade getFacadeProxy() { 160 | return facadeProxy; 161 | } 162 | 163 | protected ReadIsolationHandler getReadIsolationHandler(IsolationLevel isolationLevel) { 164 | if (isolationLevel == null) { 165 | throw new IllegalArgumentException("isolation level is required"); 166 | } 167 | switch (isolationLevel) { 168 | case UNCOMMITTED: 169 | return readUncommittedIsolationHandler; 170 | case COMMITTED: 171 | return readCommittedIsolationHandler; 172 | case READ_LOCK: 173 | throw new IllegalArgumentException("Cannot call getItem at the READ_LOCK isolation level outside of a transaction. Call getItem on a transaction directly instead."); 174 | default: 175 | throw new IllegalArgumentException("Unrecognized isolation level: " + isolationLevel); 176 | } 177 | } 178 | 179 | public GetItemResult getItem(GetItemRequest request, IsolationLevel isolationLevel) { 180 | if (request.getAttributesToGet() != null) { 181 | Set attributesToGet = new HashSet(request.getAttributesToGet()); 182 | attributesToGet.addAll(Transaction.SPECIAL_ATTR_NAMES); 183 | request.setAttributesToGet(attributesToGet); 184 | } 185 | GetItemResult result = getClient().getItem(request); 186 | Map item = getReadIsolationHandler(isolationLevel).handleItem(result.getItem(), request.getAttributesToGet(), request.getTableName()); 187 | Transaction.stripSpecialAttributes(item); 188 | result.setItem(item); 189 | return result; 190 | } 191 | 192 | public String getTransactionTableName() { 193 | return transactionTableName; 194 | } 195 | 196 | public String getItemImageTableName() { 197 | return itemImageTableName; 198 | } 199 | 200 | /** 201 | * Breaks an item lock and leaves the item intact, leaving an item in an unknown state. Only works if the owning transaction 202 | * does not exist. 203 | * 204 | * 1) It could leave an item that should not exist (was inserted only for obtaining the lock) 205 | * 2) It could replace the item with an old copy of the item from an unknown previous transaction 206 | * 3) A request from an earlier transaction could be applied a second time 207 | * 4) Other conditions of this nature 208 | * 209 | * @param tableName 210 | * @param item 211 | * @param txId 212 | */ 213 | public void breakLock(String tableName, Map item, String txId) { 214 | if(log.isWarnEnabled()) { 215 | log.warn("Breaking a lock on table " + tableName + " for transaction " + txId + " for item " + item + ". This will leave the item in an unknown state"); 216 | } 217 | Transaction.unlockItemUnsafe(this, tableName, item, txId); 218 | } 219 | 220 | public static void verifyOrCreateTransactionTable(AmazonDynamoDB client, String tableName, long readCapacityUnits, long writeCapacityUnits, Long waitTimeSeconds) throws InterruptedException { 221 | new TableHelper(client).verifyOrCreateTable( 222 | tableName, 223 | TRANSACTIONS_TABLE_ATTRIBUTES, 224 | TRANSACTIONS_TABLE_KEY_SCHEMA, 225 | null/*localIndexes*/, 226 | new ProvisionedThroughput() 227 | .withReadCapacityUnits(readCapacityUnits) 228 | .withWriteCapacityUnits(writeCapacityUnits), 229 | waitTimeSeconds); 230 | } 231 | 232 | public static void verifyOrCreateTransactionImagesTable(AmazonDynamoDB client, String tableName, long readCapacityUnits, long writeCapacityUnits, Long waitTimeSeconds) throws InterruptedException { 233 | new TableHelper(client).verifyOrCreateTable( 234 | tableName, 235 | TRANSACTION_IMAGES_TABLE_ATTRIBUTES, 236 | TRANSACTION_IMAGES_TABLE_KEY_SCHEMA, 237 | null/*localIndexes*/, 238 | new ProvisionedThroughput() 239 | .withReadCapacityUnits(readCapacityUnits) 240 | .withWriteCapacityUnits(writeCapacityUnits), 241 | waitTimeSeconds); 242 | } 243 | 244 | /** 245 | * Ensures that the transaction table exists and has the correct schema. 246 | * 247 | * @param client 248 | * @param transactionTableName 249 | * @param transactionImagesTableName 250 | * @throws ResourceInUseException if the table exists but has the wrong schema 251 | * @throws ResourceNotFoundException if the table does not exist 252 | */ 253 | public static void verifyTransactionTablesExist(AmazonDynamoDB client, String transactionTableName, String transactionImagesTableName) { 254 | String state = new TableHelper(client).verifyTableExists(transactionTableName, TRANSACTIONS_TABLE_ATTRIBUTES, TRANSACTIONS_TABLE_KEY_SCHEMA, null/*localIndexes*/); 255 | if(! "ACTIVE".equals(state)) { 256 | throw new ResourceInUseException("Table " + transactionTableName + " is not ACTIVE"); 257 | } 258 | 259 | state = new TableHelper(client).verifyTableExists(transactionImagesTableName, TRANSACTION_IMAGES_TABLE_ATTRIBUTES, TRANSACTION_IMAGES_TABLE_KEY_SCHEMA, null/*localIndexes*/); 260 | if(! "ACTIVE".equals(state)) { 261 | throw new ResourceInUseException("Table " + transactionImagesTableName + " is not ACTIVE"); 262 | } 263 | } 264 | 265 | protected double getCurrentTime() { 266 | return System.currentTimeMillis() / 1000.0; 267 | } 268 | 269 | protected AttributeValue getCurrentTimeAttribute() { 270 | return new AttributeValue().withN(new Double(getCurrentTime()).toString()); 271 | } 272 | 273 | private static class AttributeDefinitionComparator implements Comparator { 274 | 275 | @Override 276 | public int compare(AttributeDefinition arg0, AttributeDefinition arg1) { 277 | if(arg0 == null) 278 | return -1; 279 | 280 | if(arg1 == null) 281 | return 1; 282 | 283 | int comp = arg0.getAttributeName().compareTo(arg1.getAttributeName()); 284 | if(comp != 0) 285 | return comp; 286 | 287 | comp = arg0.getAttributeType().compareTo(arg1.getAttributeType()); 288 | return comp; 289 | } 290 | 291 | } 292 | 293 | /** 294 | * Load an item outside a transaction using the mapper. 295 | * 296 | * @param item 297 | * An item where the key attributes are populated; the key 298 | * attributes from this item are used to form the GetItemRequest 299 | * to retrieve the item. 300 | * @param isolationLevel 301 | * The isolation level to use; this has the same meaning as for 302 | * {@link TransactionManager#getItem(GetItemRequest, IsolationLevel)} 303 | * . 304 | * @return An instance of the item class with all attributes populated from 305 | * the table, or null if the item does not exist. 306 | */ 307 | public T load(T item, 308 | IsolationLevel isolationLevel) { 309 | try { 310 | getFacadeProxy().setBackend(new TransactionManagerDynamoDBFacade(this, isolationLevel)); 311 | return getClientMapper().load(item); 312 | } finally { 313 | getFacadeProxy().setBackend(null); 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/TransactionManagerDynamoDBFacade.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014 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.AmazonClientException; 8 | import com.amazonaws.AmazonServiceException; 9 | import com.amazonaws.AmazonWebServiceRequest; 10 | import com.amazonaws.ResponseMetadata; 11 | import com.amazonaws.regions.Region; 12 | import com.amazonaws.services.dynamodbv2.AbstractAmazonDynamoDB; 13 | import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; 14 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 15 | import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; 16 | import com.amazonaws.services.dynamodbv2.model.BatchGetItemRequest; 17 | import com.amazonaws.services.dynamodbv2.model.BatchGetItemResult; 18 | import com.amazonaws.services.dynamodbv2.model.BatchWriteItemRequest; 19 | import com.amazonaws.services.dynamodbv2.model.BatchWriteItemResult; 20 | import com.amazonaws.services.dynamodbv2.model.Condition; 21 | import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; 22 | import com.amazonaws.services.dynamodbv2.model.CreateTableResult; 23 | import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; 24 | import com.amazonaws.services.dynamodbv2.model.DeleteItemResult; 25 | import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; 26 | import com.amazonaws.services.dynamodbv2.model.DeleteTableResult; 27 | import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; 28 | import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; 29 | import com.amazonaws.services.dynamodbv2.model.GetItemRequest; 30 | import com.amazonaws.services.dynamodbv2.model.GetItemResult; 31 | import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; 32 | import com.amazonaws.services.dynamodbv2.model.KeysAndAttributes; 33 | import com.amazonaws.services.dynamodbv2.model.ListTablesRequest; 34 | import com.amazonaws.services.dynamodbv2.model.ListTablesResult; 35 | import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; 36 | import com.amazonaws.services.dynamodbv2.model.PutItemRequest; 37 | import com.amazonaws.services.dynamodbv2.model.PutItemResult; 38 | import com.amazonaws.services.dynamodbv2.model.QueryRequest; 39 | import com.amazonaws.services.dynamodbv2.model.QueryResult; 40 | import com.amazonaws.services.dynamodbv2.model.ScanRequest; 41 | import com.amazonaws.services.dynamodbv2.model.ScanResult; 42 | import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest; 43 | import com.amazonaws.services.dynamodbv2.model.UpdateItemResult; 44 | import com.amazonaws.services.dynamodbv2.model.UpdateTableRequest; 45 | import com.amazonaws.services.dynamodbv2.model.UpdateTableResult; 46 | import com.amazonaws.services.dynamodbv2.model.WriteRequest; 47 | import com.amazonaws.services.dynamodbv2.transactions.Transaction.IsolationLevel; 48 | 49 | import java.util.ArrayList; 50 | import java.util.Collection; 51 | import java.util.HashMap; 52 | import java.util.HashSet; 53 | import java.util.List; 54 | import java.util.Map; 55 | import java.util.Set; 56 | 57 | /** 58 | * Facade to support the DynamoDBMapper doing a read using a specific isolation 59 | * level. Used by {@link TransactionManager#load(Object, IsolationLevel)}. 60 | */ 61 | public class TransactionManagerDynamoDBFacade extends AbstractAmazonDynamoDB { 62 | 63 | private final TransactionManager txManager; 64 | private final IsolationLevel isolationLevel; 65 | private final ReadIsolationHandler isolationHandler; 66 | 67 | public TransactionManagerDynamoDBFacade(TransactionManager txManager, IsolationLevel isolationLevel) { 68 | this.txManager = txManager; 69 | this.isolationLevel = isolationLevel; 70 | this.isolationHandler = txManager.getReadIsolationHandler(isolationLevel); 71 | } 72 | 73 | /** 74 | * Returns versions of the items can be read at the specified isolation level stripped of 75 | * special attributes. 76 | * @param items The items to check 77 | * @param tableName The table that contains the item 78 | * @param attributesToGet The attributes to get from the table. If null or empty, will 79 | * fetch all attributes. 80 | * @return Versions of the items that can be read at the isolation level stripped of special attributes 81 | */ 82 | private List> handleItems( 83 | final List> items, 84 | final String tableName, 85 | final List attributesToGet) { 86 | List> result = new ArrayList>(); 87 | for (Map item : items) { 88 | Map handledItem = isolationHandler.handleItem(item, attributesToGet, tableName); 89 | /** 90 | * If the item is null, BatchGetItems, Scan, and Query should exclude the item from 91 | * the returned list. This is based on the DynamoDB documentation. 92 | */ 93 | if (handledItem != null) { 94 | Transaction.stripSpecialAttributes(handledItem); 95 | result.add(handledItem); 96 | } 97 | } 98 | return result; 99 | } 100 | 101 | private Collection addSpecialAttributes(Collection attributesToGet) { 102 | if (attributesToGet == null) { 103 | return null; 104 | } 105 | Set result = new HashSet(attributesToGet); 106 | result.addAll(Transaction.SPECIAL_ATTR_NAMES); 107 | return result; 108 | } 109 | 110 | @Override 111 | public GetItemResult getItem(GetItemRequest request) 112 | throws AmazonServiceException, AmazonClientException { 113 | return txManager.getItem(request, isolationLevel); 114 | } 115 | 116 | @Override 117 | public GetItemResult getItem( 118 | String tableName, 119 | Map key) throws AmazonServiceException, AmazonClientException { 120 | return getItem(new GetItemRequest() 121 | .withTableName(tableName) 122 | .withKey(key)); 123 | } 124 | 125 | @Override 126 | public GetItemResult getItem(String tableName, 127 | Map key, Boolean consistentRead) 128 | throws AmazonServiceException, AmazonClientException { 129 | return getItem(new GetItemRequest() 130 | .withTableName(tableName) 131 | .withKey(key) 132 | .withConsistentRead(consistentRead)); 133 | } 134 | 135 | @Override 136 | public BatchGetItemResult batchGetItem(BatchGetItemRequest request) 137 | throws AmazonServiceException, AmazonClientException { 138 | for (KeysAndAttributes keysAndAttributes : request.getRequestItems().values()) { 139 | Collection attributesToGet = keysAndAttributes.getAttributesToGet(); 140 | keysAndAttributes.setAttributesToGet(addSpecialAttributes(attributesToGet)); 141 | } 142 | BatchGetItemResult result = txManager.getClient().batchGetItem(request); 143 | Map>> responses = new HashMap>>(); 144 | for (Map.Entry>> e : result.getResponses().entrySet()) { 145 | String tableName = e.getKey(); 146 | List attributesToGet = request.getRequestItems().get(tableName).getAttributesToGet(); 147 | List> items = handleItems(e.getValue(), tableName, attributesToGet); 148 | responses.put(tableName, items); 149 | } 150 | result.setResponses(responses); 151 | return result; 152 | } 153 | 154 | @Override 155 | public BatchGetItemResult batchGetItem( 156 | Map requestItems, 157 | String returnConsumedCapacity) throws AmazonServiceException, 158 | AmazonClientException { 159 | BatchGetItemRequest request = new BatchGetItemRequest() 160 | .withRequestItems(requestItems) 161 | .withReturnConsumedCapacity(returnConsumedCapacity); 162 | return batchGetItem(request); 163 | } 164 | 165 | @Override 166 | public BatchGetItemResult batchGetItem( 167 | Map requestItems) 168 | throws AmazonServiceException, AmazonClientException { 169 | BatchGetItemRequest request = new BatchGetItemRequest() 170 | .withRequestItems(requestItems); 171 | return batchGetItem(request); 172 | } 173 | 174 | @Override 175 | public ScanResult scan(ScanRequest request) throws AmazonServiceException, 176 | AmazonClientException { 177 | Collection attributesToGet = addSpecialAttributes(request.getAttributesToGet()); 178 | request.setAttributesToGet(attributesToGet); 179 | ScanResult result = txManager.getClient().scan(request); 180 | List> items = handleItems(result.getItems(), request.getTableName(), request.getAttributesToGet()); 181 | result.setItems(items); 182 | return result; 183 | } 184 | 185 | @Override 186 | public ScanResult scan(String tableName, List attributesToGet) 187 | throws AmazonServiceException, AmazonClientException { 188 | ScanRequest request = new ScanRequest() 189 | .withTableName(tableName) 190 | .withAttributesToGet(attributesToGet); 191 | return scan(request); 192 | } 193 | 194 | @Override 195 | public ScanResult scan(String tableName, Map scanFilter) 196 | throws AmazonServiceException, AmazonClientException { 197 | ScanRequest request = new ScanRequest() 198 | .withTableName(tableName) 199 | .withScanFilter(scanFilter); 200 | return scan(request); 201 | } 202 | 203 | @Override 204 | public ScanResult scan( 205 | String tableName, 206 | List attributesToGet, 207 | Map scanFilter) throws AmazonServiceException, AmazonClientException { 208 | ScanRequest request = new ScanRequest() 209 | .withTableName(tableName) 210 | .withAttributesToGet(attributesToGet) 211 | .withScanFilter(scanFilter); 212 | return scan(request); 213 | } 214 | 215 | @Override 216 | public QueryResult query(QueryRequest request) throws AmazonServiceException, 217 | AmazonClientException { 218 | Collection attributesToGet = addSpecialAttributes(request.getAttributesToGet()); 219 | request.setAttributesToGet(attributesToGet); 220 | QueryResult result = txManager.getClient().query(request); 221 | List> items = handleItems(result.getItems(), request.getTableName(), request.getAttributesToGet()); 222 | result.setItems(items); 223 | return result; 224 | } 225 | 226 | @Override 227 | public PutItemResult putItem(PutItemRequest request) 228 | throws AmazonServiceException, AmazonClientException { 229 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 230 | } 231 | 232 | @Override 233 | public UpdateItemResult updateItem(UpdateItemRequest request) 234 | throws AmazonServiceException, AmazonClientException { 235 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 236 | } 237 | 238 | @Override 239 | public DeleteItemResult deleteItem(DeleteItemRequest request) 240 | throws AmazonServiceException, AmazonClientException { 241 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 242 | } 243 | 244 | @Override 245 | public BatchWriteItemResult batchWriteItem(BatchWriteItemRequest arg0) 246 | throws AmazonServiceException, AmazonClientException { 247 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 248 | } 249 | 250 | @Override 251 | public CreateTableResult createTable(CreateTableRequest arg0) 252 | throws AmazonServiceException, AmazonClientException { 253 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 254 | } 255 | 256 | @Override 257 | public DeleteTableResult deleteTable(DeleteTableRequest arg0) 258 | throws AmazonServiceException, AmazonClientException { 259 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 260 | } 261 | 262 | @Override 263 | public DescribeTableResult describeTable(DescribeTableRequest arg0) 264 | throws AmazonServiceException, AmazonClientException { 265 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 266 | } 267 | 268 | @Override 269 | public ResponseMetadata getCachedResponseMetadata( 270 | AmazonWebServiceRequest arg0) { 271 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 272 | } 273 | 274 | @Override 275 | public ListTablesResult listTables() throws AmazonServiceException, 276 | AmazonClientException { 277 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 278 | } 279 | 280 | @Override 281 | public ListTablesResult listTables(ListTablesRequest arg0) 282 | throws AmazonServiceException, AmazonClientException { 283 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 284 | } 285 | 286 | @Override 287 | public void setEndpoint(String arg0) throws IllegalArgumentException { 288 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 289 | } 290 | 291 | @Override 292 | public void setRegion(Region arg0) throws IllegalArgumentException { 293 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 294 | } 295 | 296 | @Override 297 | public void shutdown() { 298 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 299 | } 300 | 301 | @Override 302 | public UpdateTableResult updateTable(UpdateTableRequest arg0) 303 | throws AmazonServiceException, AmazonClientException { 304 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 305 | } 306 | 307 | @Override 308 | public UpdateTableResult updateTable(String tableName, 309 | ProvisionedThroughput provisionedThroughput) 310 | throws AmazonServiceException, AmazonClientException { 311 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 312 | } 313 | 314 | @Override 315 | public DeleteTableResult deleteTable(String tableName) 316 | throws AmazonServiceException, AmazonClientException { 317 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 318 | } 319 | 320 | @Override 321 | public BatchWriteItemResult batchWriteItem( 322 | Map> requestItems) 323 | throws AmazonServiceException, AmazonClientException { 324 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 325 | } 326 | 327 | @Override 328 | public DescribeTableResult describeTable(String tableName) 329 | throws AmazonServiceException, AmazonClientException { 330 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 331 | } 332 | 333 | @Override 334 | public DeleteItemResult deleteItem(String tableName, 335 | Map key) throws AmazonServiceException, 336 | AmazonClientException { 337 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 338 | } 339 | 340 | @Override 341 | public DeleteItemResult deleteItem(String tableName, 342 | Map key, String returnValues) 343 | throws AmazonServiceException, AmazonClientException { 344 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 345 | } 346 | 347 | @Override 348 | public CreateTableResult createTable( 349 | List attributeDefinitions, String tableName, 350 | List keySchema, 351 | ProvisionedThroughput provisionedThroughput) 352 | throws AmazonServiceException, AmazonClientException { 353 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 354 | } 355 | 356 | @Override 357 | public PutItemResult putItem(String tableName, 358 | Map item) throws AmazonServiceException, 359 | AmazonClientException { 360 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 361 | } 362 | 363 | @Override 364 | public PutItemResult putItem(String tableName, 365 | Map item, String returnValues) 366 | throws AmazonServiceException, AmazonClientException { 367 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 368 | } 369 | 370 | @Override 371 | public ListTablesResult listTables(String exclusiveStartTableName) 372 | throws AmazonServiceException, AmazonClientException { 373 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 374 | } 375 | 376 | @Override 377 | public ListTablesResult listTables(String exclusiveStartTableName, 378 | Integer limit) throws AmazonServiceException, AmazonClientException { 379 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 380 | } 381 | 382 | @Override 383 | public ListTablesResult listTables(Integer limit) 384 | throws AmazonServiceException, AmazonClientException { 385 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 386 | } 387 | 388 | @Override 389 | public UpdateItemResult updateItem(String tableName, 390 | Map key, 391 | Map attributeUpdates) 392 | throws AmazonServiceException, AmazonClientException { 393 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 394 | } 395 | 396 | @Override 397 | public UpdateItemResult updateItem(String tableName, 398 | Map key, 399 | Map attributeUpdates, 400 | String returnValues) throws AmazonServiceException, 401 | AmazonClientException { 402 | throw new UnsupportedOperationException("Use the underlying client instance instead"); 403 | } 404 | 405 | } 406 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/exceptions/DuplicateRequestException.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.exceptions; 6 | 7 | 8 | public class DuplicateRequestException extends TransactionException { 9 | 10 | private static final long serialVersionUID = 5461061207526371210L; 11 | 12 | public DuplicateRequestException(String txId, String tableName, String key) { 13 | super(txId, "Duplicate request for table name " + tableName + " for key " + key); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/exceptions/InvalidRequestException.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.exceptions; 6 | 7 | import java.util.Map; 8 | 9 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 10 | import com.amazonaws.services.dynamodbv2.transactions.Request; 11 | 12 | public class InvalidRequestException extends TransactionException { 13 | 14 | private static final long serialVersionUID = 4622315126910271817L; 15 | 16 | private final String tableName; 17 | private final Map key; 18 | private final Request request; 19 | 20 | public InvalidRequestException(String message, String txId, String tableName, Map key, Request request) { 21 | this(message, txId, tableName, key, request, null); 22 | } 23 | 24 | public InvalidRequestException(String message, String txId, String tableName, Map key, Request request, Throwable t) { 25 | super(((message != null) ? ": " + message : "Invalid request") + " for transaction " + txId + " table " + tableName + " key " + key, t); 26 | this.tableName = tableName; 27 | this.key = key; 28 | this.request = request; 29 | } 30 | 31 | public String getTableName() { 32 | return tableName; 33 | } 34 | 35 | public Map getKey() { 36 | return key; 37 | } 38 | 39 | public Request getRequest() { 40 | return request; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/exceptions/ItemNotLockedException.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.exceptions; 6 | 7 | import java.util.Map; 8 | 9 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 10 | 11 | /** 12 | * Indicates that the transaction could not get the lock because it is owned by another transaction. 13 | */ 14 | public class ItemNotLockedException extends TransactionException { 15 | 16 | private static final long serialVersionUID = -2992047273290608776L; 17 | 18 | private final String txId; 19 | private final String lockOwnerTxId; 20 | private final String tableName; 21 | private final Map item; 22 | 23 | public ItemNotLockedException(String txId, String lockTxId, String tableName, Map item) { 24 | this(txId, lockTxId, tableName, item, null); 25 | } 26 | 27 | public ItemNotLockedException(String txId, String lockOwnerTxId, String tableName, Map item, Throwable t) { 28 | super(txId, "Item is not locked by our transaction, is locked by " + lockOwnerTxId + " for table " + tableName + ", item: "+ item); 29 | this.txId = txId; 30 | this.lockOwnerTxId = lockOwnerTxId; 31 | this.tableName = tableName; 32 | this.item = item; 33 | } 34 | 35 | public String getTxId() { 36 | return txId; 37 | } 38 | 39 | public String getLockOwnerTxId() { 40 | return lockOwnerTxId; 41 | } 42 | 43 | public Map getItem() { 44 | return item; 45 | } 46 | 47 | public String getTableName() { 48 | return tableName; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/exceptions/TransactionAssertionException.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.exceptions; 6 | 7 | public class TransactionAssertionException extends TransactionException { 8 | 9 | private static final long serialVersionUID = -894664265849460781L; 10 | 11 | public TransactionAssertionException(String txId, String message) { 12 | super(txId, message); 13 | } 14 | 15 | /** 16 | * Throws an assertion exception with a message constructed from to toString() of each data pair, if the assertion is false. 17 | * @param assertion 18 | * @param txId 19 | * @param message 20 | * @param data 21 | */ 22 | public static void txAssert(boolean assertion, String txId, String message, Object... data) { 23 | if(! assertion) { 24 | if(data != null) { 25 | StringBuilder sb = new StringBuilder(); 26 | for(Object d : data) { 27 | sb.append(d); 28 | sb.append(", "); 29 | } 30 | message = message + " - " + sb.toString(); 31 | } 32 | 33 | throw new TransactionAssertionException(txId, message); 34 | } 35 | } 36 | 37 | public static void txFail(String txId, String message, Object... data) { 38 | txAssert(false, txId, message, data); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/exceptions/TransactionCommittedException.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.exceptions; 6 | 7 | /** 8 | * Thrown when a transaction was attempted to be rolled back, but it actually completed. 9 | */ 10 | public class TransactionCommittedException extends TransactionCompletedException { 11 | 12 | private static final long serialVersionUID = 1628959201410733660L; 13 | 14 | public TransactionCommittedException(String txId, String message) { 15 | super(txId, message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/exceptions/TransactionCompletedException.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.exceptions; 6 | 7 | /** 8 | * Thrown when a transaction is completed (either committed or rolled back) and it wasn't the expectation of the caller 9 | * for this to happen . 10 | */ 11 | public class TransactionCompletedException extends TransactionException { 12 | 13 | private static final long serialVersionUID = -8170993155989412979L; 14 | 15 | public TransactionCompletedException(String txId, String message) { 16 | super(txId, message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/exceptions/TransactionException.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.exceptions; 6 | 7 | public class TransactionException extends RuntimeException { 8 | 9 | private static final long serialVersionUID = -3886636775903901771L; 10 | 11 | private final String txId; 12 | 13 | public TransactionException(String txId, String message) { 14 | super(txId + " - " + message); 15 | this.txId = txId; 16 | } 17 | 18 | public TransactionException(String txId, String message, Throwable t) { 19 | super(txId + " - " + message, t); 20 | this.txId = txId; 21 | } 22 | 23 | public TransactionException(String txId, Throwable t) { 24 | super(txId + " - " + ((t != null) ? t.getMessage() : ""), t); 25 | this.txId = txId; 26 | } 27 | 28 | public String getTxId() { 29 | return txId; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/exceptions/TransactionNotFoundException.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.exceptions; 6 | 7 | 8 | /** 9 | * Indicates that the transaction record no longer exists (or never did) 10 | */ 11 | public class TransactionNotFoundException extends TransactionException { 12 | 13 | private static final long serialVersionUID = 1482803351154923519L; 14 | 15 | public TransactionNotFoundException(String txId) { 16 | super(txId, "Transaction not found"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/exceptions/TransactionRolledBackException.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.exceptions; 6 | 7 | /** 8 | * Thrown when a transaction was attempted to be committed, but it actually rolled back. 9 | */ 10 | public class TransactionRolledBackException extends TransactionCompletedException { 11 | 12 | private static final long serialVersionUID = 1628959201410733660L; 13 | 14 | public TransactionRolledBackException(String txId, String message) { 15 | super(txId, message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/transactions/exceptions/UnknownCompletedTransactionException.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.exceptions; 6 | 7 | /** 8 | * Thrown when a transaction is no longer pending, but it is not known whether it committed or was rolled back. 9 | */ 10 | public class UnknownCompletedTransactionException extends TransactionCompletedException { 11 | 12 | private static final long serialVersionUID = 612575052603020091L; 13 | 14 | public UnknownCompletedTransactionException(String txId, String message) { 15 | super(txId, message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/util/ImmutableAttributeValue.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.util; 6 | 7 | import java.nio.ByteBuffer; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 13 | 14 | /** 15 | * An immutable class that can be used in map keys. Does a copy of the attribute value 16 | * to prevent any member from being mutated. 17 | */ 18 | public class ImmutableAttributeValue { 19 | 20 | private final String n; 21 | private final String s; 22 | private final byte[] b; 23 | private final List ns; 24 | private final List ss; 25 | private final List bs; 26 | 27 | public ImmutableAttributeValue(AttributeValue av) { 28 | s = av.getS(); 29 | n = av.getN(); 30 | b = av.getB() != null ? av.getB().array().clone() : null; 31 | ns = av.getNS() != null ? new ArrayList(av.getNS()) : null; 32 | ss = av.getSS() != null ? new ArrayList(av.getSS()) : null; 33 | bs = av.getBS() != null ? new ArrayList(av.getBS().size()) : null; 34 | 35 | if(av.getBS() != null) { 36 | for(ByteBuffer buf : av.getBS()) { 37 | if(buf != null) { 38 | bs.add(buf.array().clone()); 39 | } else { 40 | bs.add(null); 41 | } 42 | } 43 | } 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | final int prime = 31; 49 | int result = 1; 50 | result = prime * result + Arrays.hashCode(b); 51 | result = prime * result + ((bs == null) ? 0 : bs.hashCode()); 52 | result = prime * result + ((n == null) ? 0 : n.hashCode()); 53 | result = prime * result + ((ns == null) ? 0 : ns.hashCode()); 54 | result = prime * result + ((s == null) ? 0 : s.hashCode()); 55 | result = prime * result + ((ss == null) ? 0 : ss.hashCode()); 56 | return result; 57 | } 58 | 59 | @Override 60 | public boolean equals(Object obj) { 61 | if (this == obj) 62 | return true; 63 | if (obj == null) 64 | return false; 65 | if (getClass() != obj.getClass()) 66 | return false; 67 | ImmutableAttributeValue other = (ImmutableAttributeValue) obj; 68 | if (!Arrays.equals(b, other.b)) 69 | return false; 70 | if (bs == null) { 71 | if (other.bs != null) 72 | return false; 73 | } 74 | else if (!bs.equals(other.bs)) { 75 | // Note: this else if block is not auto-generated 76 | if(other.bs == null) 77 | return false; 78 | if(bs.size() != other.bs.size()) 79 | return false; 80 | for(int i = 0; i < bs.size(); i++) { 81 | if (!Arrays.equals(bs.get(i), other.bs.get(i))) 82 | return false; 83 | } 84 | return true; 85 | } 86 | if (n == null) { 87 | if (other.n != null) 88 | return false; 89 | } 90 | else if (!n.equals(other.n)) 91 | return false; 92 | if (ns == null) { 93 | if (other.ns != null) 94 | return false; 95 | } 96 | else if (!ns.equals(other.ns)) 97 | return false; 98 | if (s == null) { 99 | if (other.s != null) 100 | return false; 101 | } 102 | else if (!s.equals(other.s)) 103 | return false; 104 | if (ss == null) { 105 | if (other.ss != null) 106 | return false; 107 | } 108 | else if (!ss.equals(other.ss)) 109 | return false; 110 | return true; 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/util/ImmutableKey.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.util; 6 | 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 12 | 13 | /** 14 | * An immutable, write-only key map for storing DynamoDB a primary key value as a map key in other maps. 15 | */ 16 | public class ImmutableKey { 17 | 18 | private final Map key; 19 | 20 | public ImmutableKey(Map mutableKey) { 21 | if(mutableKey == null) { 22 | this.key = null; 23 | } else { 24 | Map keyBuilder = new HashMap(mutableKey.size()); 25 | for(Map.Entry e : mutableKey.entrySet()) { 26 | keyBuilder.put(e.getKey(), new ImmutableAttributeValue(e.getValue())); 27 | } 28 | this.key = Collections.unmodifiableMap(keyBuilder); 29 | } 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | final int prime = 31; 35 | int result = 1; 36 | result = prime * result + ((key == null) ? 0 : key.hashCode()); 37 | return result; 38 | } 39 | 40 | @Override 41 | public boolean equals(Object obj) { 42 | if (this == obj) 43 | return true; 44 | if (obj == null) 45 | return false; 46 | if (getClass() != obj.getClass()) 47 | return false; 48 | ImmutableKey other = (ImmutableKey) obj; 49 | if (key == null) { 50 | if (other.key != null) 51 | return false; 52 | } 53 | else if (!key.equals(other.key)) 54 | return false; 55 | return true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/util/TableHelper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | package com.amazonaws.services.dynamodbv2.util; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashSet; 9 | import java.util.List; 10 | 11 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; 12 | import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; 13 | import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; 14 | import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; 15 | import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; 16 | import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; 17 | import com.amazonaws.services.dynamodbv2.model.LocalSecondaryIndex; 18 | import com.amazonaws.services.dynamodbv2.model.LocalSecondaryIndexDescription; 19 | import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; 20 | import com.amazonaws.services.dynamodbv2.model.ResourceInUseException; 21 | import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; 22 | import com.amazonaws.services.dynamodbv2.model.TableStatus; 23 | 24 | public class TableHelper { 25 | 26 | private final AmazonDynamoDB client; 27 | 28 | public TableHelper(AmazonDynamoDB client) { 29 | if(client == null) { 30 | throw new IllegalArgumentException("client must not be null"); 31 | } 32 | this.client = client; 33 | } 34 | 35 | public String verifyTableExists( 36 | String tableName, 37 | List definitions, 38 | List keySchema, 39 | List localIndexes) { 40 | 41 | DescribeTableResult describe = client.describeTable(new DescribeTableRequest().withTableName(tableName)); 42 | if(! new HashSet(definitions).equals(new HashSet(describe.getTable().getAttributeDefinitions()))) { 43 | throw new ResourceInUseException("Table " + tableName + " had the wrong AttributesToGet." 44 | + " Expected: " + definitions + " " 45 | + " Was: " + describe.getTable().getAttributeDefinitions()); 46 | } 47 | 48 | if(! keySchema.equals(describe.getTable().getKeySchema())) { 49 | throw new ResourceInUseException("Table " + tableName + " had the wrong KeySchema." 50 | + " Expected: " + keySchema + " " 51 | + " Was: " + describe.getTable().getKeySchema()); 52 | } 53 | 54 | List theirLSIs = null; 55 | if(describe.getTable().getLocalSecondaryIndexes() != null) { 56 | theirLSIs = new ArrayList(); 57 | for(LocalSecondaryIndexDescription description : describe.getTable().getLocalSecondaryIndexes()) { 58 | LocalSecondaryIndex lsi = new LocalSecondaryIndex() 59 | .withIndexName(description.getIndexName()) 60 | .withKeySchema(description.getKeySchema()) 61 | .withProjection(description.getProjection()); 62 | theirLSIs.add(lsi); 63 | } 64 | } 65 | 66 | if(localIndexes != null) { 67 | if(! new HashSet(localIndexes).equals(new HashSet(theirLSIs))) { 68 | throw new ResourceInUseException("Table " + tableName + " did not have the expected LocalSecondaryIndexes." 69 | + " Expected: " + localIndexes 70 | + " Was: " + theirLSIs); 71 | } 72 | } else { 73 | if(theirLSIs != null) { 74 | throw new ResourceInUseException("Table " + tableName + " had local secondary indexes, but expected none." 75 | + " Indexes: " + theirLSIs); 76 | } 77 | } 78 | 79 | return describe.getTable().getTableStatus(); 80 | } 81 | 82 | /** 83 | * Verifies that the table exists with the specified schema, and creates it if it does not exist. 84 | * 85 | * @param tableName 86 | * @param definitions 87 | * @param keySchema 88 | * @param localIndexes 89 | * @param provisionedThroughput 90 | * @param waitTimeSeconds 91 | * @throws InterruptedException 92 | */ 93 | public void verifyOrCreateTable( 94 | String tableName, 95 | List definitions, 96 | List keySchema, 97 | List localIndexes, 98 | ProvisionedThroughput provisionedThroughput, 99 | Long waitTimeSeconds) throws InterruptedException { 100 | 101 | if(waitTimeSeconds != null && waitTimeSeconds < 0) { 102 | throw new IllegalArgumentException("Invalid waitTimeSeconds " + waitTimeSeconds); 103 | } 104 | 105 | String status = null; 106 | try { 107 | status = verifyTableExists(tableName, definitions, keySchema, localIndexes); 108 | } catch(ResourceNotFoundException e) { 109 | status = client.createTable(new CreateTableRequest() 110 | .withTableName(tableName) 111 | .withAttributeDefinitions(definitions) 112 | .withKeySchema(keySchema) 113 | .withLocalSecondaryIndexes(localIndexes) 114 | .withProvisionedThroughput(provisionedThroughput)).getTableDescription().getTableStatus(); 115 | } 116 | 117 | if(waitTimeSeconds != null && ! TableStatus.ACTIVE.toString().equals(status)) { 118 | waitForTableActive(tableName, definitions, keySchema, localIndexes, waitTimeSeconds); 119 | } 120 | } 121 | 122 | public void waitForTableActive(String tableName, long waitTimeSeconds) throws InterruptedException { 123 | if(waitTimeSeconds < 0) { 124 | throw new IllegalArgumentException("Invalid waitTimeSeconds " + waitTimeSeconds); 125 | } 126 | 127 | long startTimeMs = System.currentTimeMillis(); 128 | long elapsedMs = 0; 129 | do { 130 | DescribeTableResult describe = client.describeTable(new DescribeTableRequest().withTableName(tableName)); 131 | String status = describe.getTable().getTableStatus(); 132 | if(TableStatus.ACTIVE.toString().equals(status)) { 133 | return; 134 | } 135 | if(TableStatus.DELETING.toString().equals(status)) { 136 | throw new ResourceInUseException("Table " + tableName + " is " + status + ", and waiting for it to become ACTIVE is not useful."); 137 | } 138 | Thread.sleep(10 * 1000); 139 | elapsedMs = System.currentTimeMillis() - startTimeMs; 140 | } while(elapsedMs / 1000.0 < waitTimeSeconds); 141 | 142 | throw new ResourceInUseException("Table " + tableName + " did not become ACTIVE after " + waitTimeSeconds + " seconds."); 143 | } 144 | 145 | public void waitForTableActive(String tableName, 146 | List definitions, 147 | List keySchema, 148 | List localIndexes, 149 | long waitTimeSeconds) throws InterruptedException { 150 | 151 | if(waitTimeSeconds < 0) { 152 | throw new IllegalArgumentException("Invalid waitTimeSeconds " + waitTimeSeconds); 153 | } 154 | 155 | long startTimeMs = System.currentTimeMillis(); 156 | long elapsedMs = 0; 157 | do { 158 | String status = verifyTableExists(tableName, definitions, keySchema, localIndexes); 159 | if(TableStatus.ACTIVE.toString().equals(status)) { 160 | return; 161 | } 162 | if(TableStatus.DELETING.toString().equals(status)) { 163 | throw new ResourceInUseException("Table " + tableName + " is " + status + ", and waiting for it to become ACTIVE is not useful."); 164 | } 165 | Thread.sleep(10 * 1000); 166 | elapsedMs = System.currentTimeMillis() - startTimeMs; 167 | } while(elapsedMs / 1000.0 < waitTimeSeconds); 168 | 169 | throw new ResourceInUseException("Table " + tableName + " did not become ACTIVE after " + waitTimeSeconds + " seconds."); 170 | } 171 | 172 | public void waitForTableDeleted(String tableName, long waitTimeSeconds) throws InterruptedException { 173 | 174 | if(waitTimeSeconds < 0) { 175 | throw new IllegalArgumentException("Invalid waitTimeSeconds " + waitTimeSeconds); 176 | } 177 | 178 | long startTimeMs = System.currentTimeMillis(); 179 | long elapsedMs = 0; 180 | do { 181 | try { 182 | DescribeTableResult describe = client.describeTable(new DescribeTableRequest().withTableName(tableName)); 183 | String status = describe.getTable().getTableStatus(); 184 | if(! TableStatus.DELETING.toString().equals(status)) { 185 | throw new ResourceInUseException("Table " + tableName + " is " + status + ", and waiting for it to not exist is only useful if it is DELETING."); 186 | } 187 | } catch (ResourceNotFoundException e) { 188 | return; 189 | } 190 | Thread.sleep(10 * 1000); 191 | elapsedMs = System.currentTimeMillis() - startTimeMs; 192 | } while(elapsedMs / 1000.0 < waitTimeSeconds); 193 | 194 | throw new ResourceInUseException("Table " + tableName + " was not deleted after " + waitTimeSeconds + " seconds."); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/transactions/ReadCommittedIsolationHandlerImplUnitTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2016 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.services.dynamodbv2.AmazonDynamoDB; 8 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 9 | import com.amazonaws.services.dynamodbv2.model.GetItemRequest; 10 | import com.amazonaws.services.dynamodbv2.model.GetItemResult; 11 | import com.amazonaws.services.dynamodbv2.transactions.TransactionItem.State; 12 | import com.amazonaws.services.dynamodbv2.transactions.exceptions.TransactionAssertionException; 13 | import com.amazonaws.services.dynamodbv2.transactions.exceptions.TransactionException; 14 | import com.amazonaws.services.dynamodbv2.transactions.exceptions.TransactionNotFoundException; 15 | import com.amazonaws.services.dynamodbv2.transactions.exceptions.UnknownCompletedTransactionException; 16 | import org.junit.Before; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | import org.mockito.Mock; 20 | import org.mockito.runners.MockitoJUnitRunner; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | import static com.amazonaws.services.dynamodbv2.transactions.ReadUncommittedIsolationHandlerImplUnitTest.KEY; 28 | import static com.amazonaws.services.dynamodbv2.transactions.ReadUncommittedIsolationHandlerImplUnitTest.NON_TRANSIENT_APPLIED_ITEM; 29 | import static com.amazonaws.services.dynamodbv2.transactions.ReadUncommittedIsolationHandlerImplUnitTest.TABLE_NAME; 30 | import static com.amazonaws.services.dynamodbv2.transactions.ReadUncommittedIsolationHandlerImplUnitTest.TRANSIENT_APPLIED_ITEM; 31 | import static com.amazonaws.services.dynamodbv2.transactions.ReadUncommittedIsolationHandlerImplUnitTest.TRANSIENT_UNAPPLIED_ITEM; 32 | import static com.amazonaws.services.dynamodbv2.transactions.ReadUncommittedIsolationHandlerImplUnitTest.TX_ID; 33 | import static com.amazonaws.services.dynamodbv2.transactions.ReadUncommittedIsolationHandlerImplUnitTest.UNLOCKED_ITEM; 34 | import static org.junit.Assert.assertEquals; 35 | import static org.junit.Assert.assertNull; 36 | import static org.junit.Assert.assertTrue; 37 | import static org.mockito.Mockito.doReturn; 38 | import static org.mockito.Mockito.doThrow; 39 | import static org.mockito.Mockito.spy; 40 | import static org.mockito.Mockito.times; 41 | import static org.mockito.Mockito.verify; 42 | import static org.mockito.Mockito.when; 43 | 44 | @RunWith(MockitoJUnitRunner.class) 45 | public class ReadCommittedIsolationHandlerImplUnitTest { 46 | 47 | protected static final int RID = 1; 48 | protected static GetItemRequest GET_ITEM_REQUEST = new GetItemRequest() 49 | .withTableName(TABLE_NAME) 50 | .withKey(KEY) 51 | .withConsistentRead(true); 52 | 53 | @Mock 54 | private TransactionManager mockTxManager; 55 | 56 | @Mock 57 | private Transaction mockTx; 58 | 59 | @Mock 60 | private TransactionItem mockTxItem; 61 | 62 | @Mock 63 | private Request mockRequest; 64 | 65 | @Mock 66 | private AmazonDynamoDB mockClient; 67 | 68 | private ReadCommittedIsolationHandlerImpl isolationHandler; 69 | 70 | @Before 71 | public void setup() { 72 | isolationHandler = spy(new ReadCommittedIsolationHandlerImpl(mockTxManager, 0)); 73 | when(mockTx.getTxItem()).thenReturn(mockTxItem); 74 | when(mockTx.getId()).thenReturn(TX_ID); 75 | when(mockTxManager.getClient()).thenReturn(mockClient); 76 | } 77 | 78 | @Test 79 | public void checkItemCommittedReturnsNullForNullItem() { 80 | assertNull(isolationHandler.checkItemCommitted(null)); 81 | } 82 | 83 | @Test 84 | public void checkItemCommittedReturnsItemForUnlockedItem() { 85 | assertEquals(UNLOCKED_ITEM, isolationHandler.checkItemCommitted(UNLOCKED_ITEM)); 86 | } 87 | 88 | @Test 89 | public void checkItemCommittedReturnsNullForTransientItem() { 90 | assertNull(isolationHandler.checkItemCommitted(TRANSIENT_APPLIED_ITEM)); 91 | assertNull(isolationHandler.checkItemCommitted(TRANSIENT_UNAPPLIED_ITEM)); 92 | } 93 | 94 | @Test(expected = TransactionException.class) 95 | public void checkItemCommittedThrowsExceptionForNonTransientAppliedItem() { 96 | isolationHandler.checkItemCommitted(NON_TRANSIENT_APPLIED_ITEM); 97 | } 98 | 99 | @Test 100 | public void filterAttributesToGetReturnsNullForNullItem() { 101 | isolationHandler.filterAttributesToGet(null, null); 102 | } 103 | 104 | @Test 105 | public void filterAttributesToGetReturnsItemWhenAttributesToGetIsNull() { 106 | Map result = isolationHandler.filterAttributesToGet(UNLOCKED_ITEM, null); 107 | assertEquals(UNLOCKED_ITEM, result); 108 | } 109 | 110 | @Test 111 | public void filterAttributesToGetReturnsItemWhenAttributesToGetIsEmpty() { 112 | Map result = isolationHandler.filterAttributesToGet(UNLOCKED_ITEM, new ArrayList()); 113 | assertEquals(UNLOCKED_ITEM, result); 114 | } 115 | 116 | @Test 117 | public void filterAttributesToGetReturnsItemWhenAttributesToGetContainsAllAttributes() { 118 | List attributesToGet = Arrays.asList("Id", "attr1"); // all attributes 119 | Map result = isolationHandler.filterAttributesToGet(UNLOCKED_ITEM, attributesToGet); 120 | assertEquals(UNLOCKED_ITEM, result); 121 | } 122 | 123 | @Test 124 | public void filterAttributesToGetReturnsOnlySpecifiedAttributesWhenSpecified() { 125 | List attributesToGet = Arrays.asList("Id"); // only keep the key 126 | Map result = isolationHandler.filterAttributesToGet(UNLOCKED_ITEM, attributesToGet); 127 | assertEquals(KEY, result); 128 | } 129 | 130 | @Test(expected = TransactionAssertionException.class) 131 | public void getOldCommittedItemThrowsExceptionIfNoLockingRequestExists() { 132 | when(mockTxItem.getRequestForKey(TABLE_NAME, KEY)).thenReturn(null); 133 | isolationHandler.getOldCommittedItem(mockTx, TABLE_NAME, KEY); 134 | } 135 | 136 | @Test(expected = UnknownCompletedTransactionException.class) 137 | public void getOldCommittedItemThrowsExceptionIfOldItemDoesNotExist() { 138 | when(mockTxItem.getRequestForKey(TABLE_NAME, KEY)).thenReturn(mockRequest); 139 | when(mockRequest.getRid()).thenReturn(RID); 140 | when(mockTxItem.loadItemImage(RID)).thenReturn(null); 141 | isolationHandler.getOldCommittedItem(mockTx, TABLE_NAME, KEY); 142 | } 143 | 144 | @Test 145 | public void getOldCommittedItemReturnsOldImageIfOldItemExists() { 146 | when(mockTxItem.getRequestForKey(TABLE_NAME, KEY)).thenReturn(mockRequest); 147 | when(mockRequest.getRid()).thenReturn(RID); 148 | when(mockTxItem.loadItemImage(RID)).thenReturn(UNLOCKED_ITEM); 149 | Map result = isolationHandler.getOldCommittedItem(mockTx, TABLE_NAME, KEY); 150 | assertEquals(UNLOCKED_ITEM, result); 151 | } 152 | 153 | @Test 154 | public void createGetItemRequestCorrectlyCreatesRequest() { 155 | when(mockTxManager.createKeyMap(TABLE_NAME, NON_TRANSIENT_APPLIED_ITEM)).thenReturn(KEY); 156 | GetItemRequest request = isolationHandler.createGetItemRequest(TABLE_NAME, NON_TRANSIENT_APPLIED_ITEM); 157 | assertEquals(TABLE_NAME, request.getTableName()); 158 | assertEquals(KEY, request.getKey()); 159 | assertEquals(null, request.getAttributesToGet()); 160 | assertTrue(request.getConsistentRead()); 161 | } 162 | 163 | @Test 164 | public void handleItemReturnsNullForNullItem() { 165 | assertNull(isolationHandler.handleItem(null, TABLE_NAME, 0)); 166 | } 167 | 168 | @Test 169 | public void handleItemReturnsItemForUnlockedItem() { 170 | assertEquals(UNLOCKED_ITEM, isolationHandler.handleItem(UNLOCKED_ITEM, TABLE_NAME, 0)); 171 | } 172 | 173 | @Test 174 | public void handleItemReturnsNullForTransientItem() { 175 | assertNull(isolationHandler.handleItem(TRANSIENT_APPLIED_ITEM, TABLE_NAME, 0)); 176 | assertNull(isolationHandler.handleItem(TRANSIENT_UNAPPLIED_ITEM, TABLE_NAME, 0)); 177 | } 178 | 179 | @Test(expected = TransactionException.class) 180 | public void handleItemThrowsExceptionForNonTransientAppliedItemWithNoCorrespondingTx() { 181 | doThrow(TransactionNotFoundException.class).when(isolationHandler).loadTransaction(TX_ID); 182 | isolationHandler.handleItem(NON_TRANSIENT_APPLIED_ITEM, TABLE_NAME, 0); 183 | } 184 | 185 | @Test 186 | public void handleItemReturnsItemForNonTransientAppliedItemWithCommittedTxItem() { 187 | doReturn(mockTx).when(isolationHandler).loadTransaction(TX_ID); 188 | when(mockTxItem.getState()).thenReturn(State.COMMITTED); 189 | assertEquals(NON_TRANSIENT_APPLIED_ITEM, isolationHandler.handleItem(NON_TRANSIENT_APPLIED_ITEM, TABLE_NAME, 0)); 190 | } 191 | 192 | @Test 193 | public void handleItemReturnsOldVersionOfItemForNonTransientAppliedItemWithPendingTxItem() { 194 | doReturn(mockTx).when(isolationHandler).loadTransaction(TX_ID); 195 | doReturn(UNLOCKED_ITEM).when(isolationHandler).getOldCommittedItem(mockTx, TABLE_NAME, KEY); 196 | when(mockTxManager.createKeyMap(TABLE_NAME, NON_TRANSIENT_APPLIED_ITEM)).thenReturn(KEY); 197 | when(mockTxItem.getState()).thenReturn(State.PENDING); 198 | when(mockTxItem.getRequestForKey(TABLE_NAME, KEY)).thenReturn(mockRequest); 199 | assertEquals(UNLOCKED_ITEM, isolationHandler.handleItem(NON_TRANSIENT_APPLIED_ITEM, TABLE_NAME, 0)); 200 | verify(isolationHandler).loadTransaction(TX_ID); 201 | } 202 | 203 | @Test(expected = TransactionException.class) 204 | public void handleItemThrowsExceptionForNonTransientAppliedItemWithPendingTxItemWithNoOldVersionAndNoRetries() { 205 | doReturn(mockTx).when(isolationHandler).loadTransaction(TX_ID); 206 | doThrow(UnknownCompletedTransactionException.class).when(isolationHandler).getOldCommittedItem(mockTx, TABLE_NAME, KEY); 207 | when(mockTxItem.getState()).thenReturn(State.PENDING); 208 | when(mockTxItem.getRequestForKey(TABLE_NAME, KEY)).thenReturn(mockRequest); 209 | isolationHandler.handleItem(NON_TRANSIENT_APPLIED_ITEM, TABLE_NAME, 0); 210 | verify(isolationHandler).loadTransaction(TX_ID); 211 | } 212 | 213 | @Test 214 | public void handleItemRetriesWhenTransactionNotFound() { 215 | doThrow(TransactionNotFoundException.class).when(isolationHandler).loadTransaction(TX_ID); 216 | when(mockTxManager.createKeyMap(TABLE_NAME, NON_TRANSIENT_APPLIED_ITEM)).thenReturn(KEY); 217 | when(mockClient.getItem(GET_ITEM_REQUEST)).thenReturn(new GetItemResult().withItem(NON_TRANSIENT_APPLIED_ITEM)); 218 | boolean caughtException = false; 219 | try { 220 | isolationHandler.handleItem(NON_TRANSIENT_APPLIED_ITEM, TABLE_NAME, 1); 221 | } catch (TransactionException e) { 222 | caughtException = true; 223 | } 224 | assertTrue(caughtException); 225 | verify(isolationHandler, times(2)).loadTransaction(TX_ID); 226 | verify(isolationHandler).createGetItemRequest(TABLE_NAME, NON_TRANSIENT_APPLIED_ITEM); 227 | verify(mockClient).getItem(GET_ITEM_REQUEST); 228 | } 229 | 230 | @Test 231 | public void handleItemRetriesWhenUnknownCompletedTransaction() { 232 | doReturn(mockTx).when(isolationHandler).loadTransaction(TX_ID); 233 | doThrow(UnknownCompletedTransactionException.class).when(isolationHandler).getOldCommittedItem(mockTx, TABLE_NAME, KEY); 234 | when(mockTxManager.createKeyMap(TABLE_NAME, NON_TRANSIENT_APPLIED_ITEM)).thenReturn(KEY); 235 | when(mockClient.getItem(GET_ITEM_REQUEST)).thenReturn(new GetItemResult().withItem(NON_TRANSIENT_APPLIED_ITEM)); 236 | boolean caughtException = false; 237 | try { 238 | isolationHandler.handleItem(NON_TRANSIENT_APPLIED_ITEM, TABLE_NAME, 1); 239 | } catch (TransactionException e) { 240 | caughtException = true; 241 | } 242 | assertTrue(caughtException); 243 | verify(isolationHandler, times(2)).loadTransaction(TX_ID); 244 | verify(isolationHandler).createGetItemRequest(TABLE_NAME, NON_TRANSIENT_APPLIED_ITEM); 245 | verify(mockClient).getItem(GET_ITEM_REQUEST); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/transactions/ReadUncommittedIsolationHandlerImplUnitTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2016 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.services.dynamodbv2.model.AttributeValue; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.runners.MockitoJUnitRunner; 12 | 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | import static org.junit.Assert.assertEquals; 18 | import static org.junit.Assert.assertNull; 19 | 20 | @RunWith(MockitoJUnitRunner.class) 21 | public class ReadUncommittedIsolationHandlerImplUnitTest { 22 | 23 | protected static final String TABLE_NAME = "TEST_TABLE"; 24 | protected static final Map KEY = Collections.singletonMap("Id", new AttributeValue().withS("KeyValue")); 25 | protected static final String TX_ID = "e1b52a78-0187-4787-b1a3-27f63a78898b"; 26 | protected static final Map UNLOCKED_ITEM = createItem(false, false, false); 27 | protected static final Map TRANSIENT_UNAPPLIED_ITEM = createItem(true, true, false); 28 | protected static final Map TRANSIENT_APPLIED_ITEM = createItem(true, true, true); 29 | protected static final Map NON_TRANSIENT_APPLIED_ITEM = createItem(true, false, true); 30 | 31 | private ReadUncommittedIsolationHandlerImpl isolationHandler; 32 | 33 | @Before 34 | public void setup() { 35 | isolationHandler = new ReadUncommittedIsolationHandlerImpl(); 36 | } 37 | 38 | private static Map createItem(boolean isLocked, boolean isTransient, boolean isApplied) { 39 | Map item = new HashMap(); 40 | if (isLocked) { 41 | item.put(Transaction.AttributeName.TXID.toString(), new AttributeValue(TX_ID)); 42 | item.put(Transaction.AttributeName.DATE.toString(), new AttributeValue().withS("")); 43 | if (isTransient) { 44 | item.put(Transaction.AttributeName.TRANSIENT.toString(), new AttributeValue().withS("")); 45 | } 46 | if (isApplied) { 47 | item.put(Transaction.AttributeName.APPLIED.toString(), new AttributeValue().withS("")); 48 | } 49 | } 50 | if (!isTransient) { 51 | item.put("attr1", new AttributeValue().withS("some value")); 52 | } 53 | item.putAll(KEY); 54 | return item; 55 | } 56 | 57 | @Test 58 | public void handleItemReturnsNullForNullItem() { 59 | assertNull(isolationHandler.handleItem(null, null, TABLE_NAME)); 60 | } 61 | 62 | @Test 63 | public void handleItemReturnsItemForUnlockedItem() { 64 | assertEquals(UNLOCKED_ITEM, isolationHandler.handleItem(UNLOCKED_ITEM, null, TABLE_NAME)); 65 | } 66 | 67 | @Test 68 | public void handleItemReturnsNullForTransientUnappliedItem() { 69 | assertNull(isolationHandler.handleItem(TRANSIENT_UNAPPLIED_ITEM, null, TABLE_NAME)); 70 | } 71 | 72 | @Test 73 | public void handleItemReturnsNullForTransientAppliedItem() { 74 | assertEquals(TRANSIENT_APPLIED_ITEM, isolationHandler.handleItem(TRANSIENT_APPLIED_ITEM, null, TABLE_NAME)); 75 | } 76 | 77 | @Test 78 | public void handleItemReturnsItemForNonTransientAppliedItem() { 79 | assertEquals(NON_TRANSIENT_APPLIED_ITEM, isolationHandler.handleItem(NON_TRANSIENT_APPLIED_ITEM, null, TABLE_NAME)); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/transactions/RequestTest.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 static org.junit.Assert.assertArrayEquals; 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertTrue; 10 | import static org.junit.Assert.fail; 11 | 12 | import java.nio.ByteBuffer; 13 | import java.util.Arrays; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | import org.junit.Test; 19 | 20 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; 21 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 22 | import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; 23 | import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; 24 | import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue; 25 | import com.amazonaws.services.dynamodbv2.model.GetItemRequest; 26 | import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; 27 | import com.amazonaws.services.dynamodbv2.model.KeyType; 28 | import com.amazonaws.services.dynamodbv2.model.PutItemRequest; 29 | import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest; 30 | import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; 31 | import com.amazonaws.services.dynamodbv2.transactions.Request.DeleteItem; 32 | import com.amazonaws.services.dynamodbv2.transactions.Request.GetItem; 33 | import com.amazonaws.services.dynamodbv2.transactions.Request.PutItem; 34 | import com.amazonaws.services.dynamodbv2.transactions.Request.UpdateItem; 35 | import com.amazonaws.services.dynamodbv2.transactions.TransactionManager; 36 | import com.amazonaws.services.dynamodbv2.transactions.exceptions.InvalidRequestException; 37 | 38 | public class RequestTest { 39 | 40 | private static final String TABLE_NAME = "Dummy"; 41 | private static final String HASH_ATTR_NAME = "Foo"; 42 | private static final List HASH_SCHEMA = Arrays.asList( 43 | new KeySchemaElement().withAttributeName(HASH_ATTR_NAME).withKeyType(KeyType.HASH)); 44 | 45 | static final Map JSON_M_ATTR_VAL = new HashMap(); 46 | private static final Map NONNULL_EXPECTED_ATTR_VALUES = new HashMap(); 47 | private static final Map NONNULL_EXP_ATTR_NAMES = new HashMap(); 48 | private static final Map NONNULL_EXP_ATTR_VALUES = new HashMap(); 49 | private static final Map BASIC_ITEM = new HashMap(); 50 | 51 | static { 52 | JSON_M_ATTR_VAL.put("attr_s", new AttributeValue().withS("s")); 53 | JSON_M_ATTR_VAL.put("attr_n", new AttributeValue().withN("1")); 54 | JSON_M_ATTR_VAL.put("attr_b", new AttributeValue().withB(ByteBuffer.wrap(new String("asdf").getBytes()))); 55 | JSON_M_ATTR_VAL.put("attr_ss", new AttributeValue().withSS("a", "b")); 56 | JSON_M_ATTR_VAL.put("attr_ns", new AttributeValue().withNS("1", "2")); 57 | JSON_M_ATTR_VAL.put("attr_bs", new AttributeValue().withBS(ByteBuffer.wrap(new String("asdf").getBytes()), ByteBuffer.wrap(new String("ghjk").getBytes()))); 58 | JSON_M_ATTR_VAL.put("attr_bool", new AttributeValue().withBOOL(true)); 59 | JSON_M_ATTR_VAL.put("attr_l", new AttributeValue().withL( 60 | new AttributeValue().withS("s"), 61 | new AttributeValue().withN("1"), 62 | new AttributeValue().withB(ByteBuffer.wrap(new String("asdf").getBytes())), 63 | new AttributeValue().withBOOL(true), 64 | new AttributeValue().withNULL(true))); 65 | JSON_M_ATTR_VAL.put("attr_null", new AttributeValue().withNULL(true)); 66 | 67 | BASIC_ITEM.put(HASH_ATTR_NAME, new AttributeValue("a")); 68 | } 69 | 70 | @Test 71 | public void validPut() { 72 | PutItem r = new PutItem(); 73 | Map item = new HashMap(); 74 | item.put(HASH_ATTR_NAME, new AttributeValue("a")); 75 | r.setRequest(new PutItemRequest() 76 | .withTableName(TABLE_NAME) 77 | .withItem(item)); 78 | r.validate("1", new MockTransactionManager(HASH_SCHEMA)); 79 | } 80 | 81 | @Test 82 | public void putNullTableName() { 83 | Map item = new HashMap(); 84 | item.put(HASH_ATTR_NAME, new AttributeValue("a")); 85 | 86 | invalidRequestTest(new PutItemRequest() 87 | .withItem(item), 88 | "TableName must not be null"); 89 | } 90 | 91 | @Test 92 | public void putNullItem() { 93 | invalidRequestTest(new PutItemRequest() 94 | .withTableName(TABLE_NAME), 95 | "PutItem must contain an Item"); 96 | } 97 | 98 | @Test 99 | public void putMissingKey() { 100 | Map item = new HashMap(); 101 | item.put("other-attr", new AttributeValue("a")); 102 | 103 | invalidRequestTest(new PutItemRequest() 104 | .withTableName(TABLE_NAME) 105 | .withItem(item), 106 | "PutItem request must contain the key attribute"); 107 | } 108 | 109 | @Test 110 | public void putExpected() { 111 | invalidRequestTest(getBasicPutRequest() 112 | .withExpected(NONNULL_EXPECTED_ATTR_VALUES), 113 | "Requests with conditions"); 114 | } 115 | 116 | @Test 117 | public void putConditionExpression() { 118 | invalidRequestTest(getBasicPutRequest() 119 | .withConditionExpression("attribute_not_exists (some_field)"), 120 | "Requests with conditions"); 121 | } 122 | 123 | @Test 124 | public void putExpressionAttributeNames() { 125 | invalidRequestTest(getBasicPutRequest() 126 | .withExpressionAttributeNames(NONNULL_EXP_ATTR_NAMES), 127 | "Requests with expressions"); 128 | } 129 | 130 | @Test 131 | public void putExpressionAttributeValues() { 132 | invalidRequestTest(getBasicPutRequest() 133 | .withExpressionAttributeValues(NONNULL_EXP_ATTR_VALUES), 134 | "Requests with expressions"); 135 | } 136 | 137 | @Test 138 | public void updateExpected() { 139 | invalidRequestTest(getBasicUpdateRequest() 140 | .withExpected(NONNULL_EXPECTED_ATTR_VALUES), 141 | "Requests with conditions"); 142 | } 143 | 144 | @Test 145 | public void updateConditionExpression() { 146 | invalidRequestTest(getBasicUpdateRequest() 147 | .withConditionExpression("attribute_not_exists(some_field)"), 148 | "Requests with conditions"); 149 | } 150 | 151 | @Test 152 | public void updateUpdateExpression() { 153 | invalidRequestTest(getBasicUpdateRequest() 154 | .withUpdateExpression("REMOVE some_field"), 155 | "Requests with expressions"); 156 | } 157 | 158 | @Test 159 | public void updateExpressionAttributeNames() { 160 | invalidRequestTest(getBasicUpdateRequest() 161 | .withExpressionAttributeNames(NONNULL_EXP_ATTR_NAMES), 162 | "Requests with expressions"); 163 | } 164 | 165 | @Test 166 | public void updateExpressionAttributeValues() { 167 | invalidRequestTest(getBasicUpdateRequest() 168 | .withExpressionAttributeValues(NONNULL_EXP_ATTR_VALUES), 169 | "Requests with expressions"); 170 | } 171 | 172 | @Test 173 | public void deleteExpected() { 174 | invalidRequestTest(getBasicDeleteRequest() 175 | .withExpected(NONNULL_EXPECTED_ATTR_VALUES), 176 | "Requests with conditions"); 177 | } 178 | 179 | @Test 180 | public void deleteConditionExpression() { 181 | invalidRequestTest(getBasicDeleteRequest() 182 | .withConditionExpression("attribute_not_exists (some_field)"), 183 | "Requests with conditions"); 184 | } 185 | 186 | @Test 187 | public void deleteExpressionAttributeNames() { 188 | invalidRequestTest(getBasicDeleteRequest() 189 | .withExpressionAttributeNames(NONNULL_EXP_ATTR_NAMES), 190 | "Requests with expressions"); 191 | } 192 | 193 | @Test 194 | public void deleteExpressionAttributeValues() { 195 | invalidRequestTest(getBasicDeleteRequest() 196 | .withExpressionAttributeValues(NONNULL_EXP_ATTR_VALUES), 197 | "Requests with expressions"); 198 | } 199 | 200 | @Test 201 | public void validUpdate() { 202 | UpdateItem r = new UpdateItem(); 203 | Map item = new HashMap(); 204 | item.put(HASH_ATTR_NAME, new AttributeValue("a")); 205 | r.setRequest(new UpdateItemRequest() 206 | .withTableName(TABLE_NAME) 207 | .withKey(item)); 208 | r.validate("1", new MockTransactionManager(HASH_SCHEMA)); 209 | } 210 | 211 | @Test 212 | public void validDelete() { 213 | DeleteItem r = new DeleteItem(); 214 | Map item = new HashMap(); 215 | item.put(HASH_ATTR_NAME, new AttributeValue("a")); 216 | r.setRequest(new DeleteItemRequest() 217 | .withTableName(TABLE_NAME) 218 | .withKey(item)); 219 | r.validate("1", new MockTransactionManager(HASH_SCHEMA)); 220 | } 221 | 222 | @Test 223 | public void validLock() { 224 | GetItem r = new GetItem(); 225 | Map item = new HashMap(); 226 | item.put(HASH_ATTR_NAME, new AttributeValue("a")); 227 | r.setRequest(new GetItemRequest() 228 | .withTableName(TABLE_NAME) 229 | .withKey(item)); 230 | r.validate("1", new MockTransactionManager(HASH_SCHEMA)); 231 | } 232 | 233 | @Test 234 | public void roundTripGetString() { 235 | GetItem r1 = new GetItem(); 236 | Map item = new HashMap(); 237 | item.put(HASH_ATTR_NAME, new AttributeValue("a")); 238 | r1.setRequest(new GetItemRequest() 239 | .withTableName(TABLE_NAME) 240 | .withKey(item)); 241 | byte[] r1Bytes = Request.serialize("123", r1).array(); 242 | Request r2 = Request.deserialize("123", ByteBuffer.wrap(r1Bytes)); 243 | byte[] r2Bytes = Request.serialize("123", r2).array(); 244 | assertArrayEquals(r1Bytes, r2Bytes); 245 | } 246 | 247 | @Test 248 | public void roundTripPutAll() { 249 | PutItem r1 = new PutItem(); 250 | Map item = new HashMap(); 251 | item.put(HASH_ATTR_NAME, new AttributeValue("a")); 252 | item.put("attr_ss", new AttributeValue().withSS("a", "b")); 253 | item.put("attr_n", new AttributeValue().withN("1")); 254 | item.put("attr_ns", new AttributeValue().withNS("1", "2")); 255 | item.put("attr_b", new AttributeValue().withB(ByteBuffer.wrap(new String("asdf").getBytes()))); 256 | item.put("attr_bs", new AttributeValue().withBS(ByteBuffer.wrap(new String("asdf").getBytes()), ByteBuffer.wrap(new String("asdf").getBytes()))); 257 | r1.setRequest(new PutItemRequest() 258 | .withTableName(TABLE_NAME) 259 | .withItem(item) 260 | .withReturnValues("ALL_OLD")); 261 | byte[] r1Bytes = Request.serialize("123", r1).array(); 262 | Request r2 = Request.deserialize("123", ByteBuffer.wrap(r1Bytes)); 263 | assertEquals(r1.getRequest(), ((PutItem)r2).getRequest()); 264 | byte[] r2Bytes = Request.serialize("123", r2).array(); 265 | assertArrayEquals(r1Bytes, r2Bytes); 266 | } 267 | 268 | @Test 269 | public void roundTripUpdateAll() { 270 | UpdateItem r1 = new UpdateItem(); 271 | Map key = new HashMap(); 272 | key.put(HASH_ATTR_NAME, new AttributeValue("a")); 273 | 274 | Map updates = new HashMap(); 275 | updates.put("attr_ss", new AttributeValueUpdate().withAction("PUT").withValue(new AttributeValue().withSS("a", "b"))); 276 | updates.put("attr_n", new AttributeValueUpdate().withAction("PUT").withValue(new AttributeValue().withN("1"))); 277 | updates.put("attr_ns", new AttributeValueUpdate().withAction("PUT").withValue(new AttributeValue().withNS("1", "2"))); 278 | updates.put("attr_b", new AttributeValueUpdate().withAction("PUT").withValue(new AttributeValue().withB(ByteBuffer.wrap(new String("asdf").getBytes())))); 279 | updates.put("attr_bs", new AttributeValueUpdate().withAction("PUT").withValue(new AttributeValue().withBS(ByteBuffer.wrap(new String("asdf").getBytes()), ByteBuffer.wrap(new String("asdf").getBytes())))); 280 | r1.setRequest(new UpdateItemRequest() 281 | .withTableName(TABLE_NAME) 282 | .withKey(key) 283 | .withAttributeUpdates(updates)); 284 | byte[] r1Bytes = Request.serialize("123", r1).array(); 285 | Request r2 = Request.deserialize("123", ByteBuffer.wrap(r1Bytes)); 286 | byte[] r2Bytes = Request.serialize("123", r2).array(); 287 | assertArrayEquals(r1Bytes, r2Bytes); 288 | } 289 | 290 | @Test 291 | public void roundTripPutAllJSON() { 292 | PutItem r1 = new PutItem(); 293 | Map item = new HashMap(); 294 | item.put(HASH_ATTR_NAME, new AttributeValue("a")); 295 | item.put("json_attr", new AttributeValue().withM(JSON_M_ATTR_VAL)); 296 | r1.setRequest(new PutItemRequest() 297 | .withTableName(TABLE_NAME) 298 | .withItem(item) 299 | .withReturnValues("ALL_OLD")); 300 | byte[] r1Bytes = Request.serialize("123", r1).array(); 301 | Request r2 = Request.deserialize("123", ByteBuffer.wrap(r1Bytes)); 302 | assertEquals(r1.getRequest(), ((PutItem)r2).getRequest()); 303 | byte[] r2Bytes = Request.serialize("123", r2).array(); 304 | assertArrayEquals(r1Bytes, r2Bytes); 305 | } 306 | 307 | @Test 308 | public void roundTripUpdateAllJSON() { 309 | UpdateItem r1 = new UpdateItem(); 310 | Map key = new HashMap(); 311 | key.put(HASH_ATTR_NAME, new AttributeValue("a")); 312 | 313 | Map updates = new HashMap(); 314 | updates.put("attr_m", new AttributeValueUpdate().withAction("PUT").withValue(new AttributeValue().withM(JSON_M_ATTR_VAL))); 315 | r1.setRequest(new UpdateItemRequest() 316 | .withTableName(TABLE_NAME) 317 | .withKey(key) 318 | .withAttributeUpdates(updates)); 319 | byte[] r1Bytes = Request.serialize("123", r1).array(); 320 | Request r2 = Request.deserialize("123", ByteBuffer.wrap(r1Bytes)); 321 | byte[] r2Bytes = Request.serialize("123", r2).array(); 322 | assertArrayEquals(r1Bytes, r2Bytes); 323 | } 324 | 325 | private PutItemRequest getBasicPutRequest() { 326 | return new PutItemRequest().withItem(BASIC_ITEM).withTableName(TABLE_NAME); 327 | } 328 | 329 | private UpdateItemRequest getBasicUpdateRequest() { 330 | return new UpdateItemRequest().withKey(BASIC_ITEM).withTableName(TABLE_NAME); 331 | } 332 | 333 | private DeleteItemRequest getBasicDeleteRequest() { 334 | return new DeleteItemRequest().withKey(BASIC_ITEM).withTableName(TABLE_NAME); 335 | } 336 | 337 | private void invalidRequestTest(PutItemRequest request, String expectedExceptionMessage) { 338 | PutItem r = new PutItem(); 339 | r.setRequest(request); 340 | try { 341 | r.validate("1", new MockTransactionManager(HASH_SCHEMA)); 342 | fail(); 343 | } catch (InvalidRequestException e) { 344 | assertTrue(e.getMessage().contains(expectedExceptionMessage)); 345 | } 346 | } 347 | 348 | private void invalidRequestTest(UpdateItemRequest request, String expectedExceptionMessage) { 349 | UpdateItem r = new UpdateItem(); 350 | r.setRequest(request); 351 | try { 352 | r.validate("1", new MockTransactionManager(HASH_SCHEMA)); 353 | fail(); 354 | } catch (InvalidRequestException e) { 355 | assertTrue(e.getMessage().contains(expectedExceptionMessage)); 356 | } 357 | } 358 | 359 | private void invalidRequestTest(DeleteItemRequest request, String expectedExceptionMessage) { 360 | DeleteItem r = new DeleteItem(); 361 | r.setRequest(request); 362 | try { 363 | r.validate("1", new MockTransactionManager(HASH_SCHEMA)); 364 | fail(); 365 | } catch (InvalidRequestException e) { 366 | assertTrue(e.getMessage().contains(expectedExceptionMessage)); 367 | } 368 | } 369 | 370 | protected class MockTransactionManager extends TransactionManager { 371 | 372 | private final List keySchema; 373 | 374 | public MockTransactionManager(List keySchema) { 375 | super(new AmazonDynamoDBClient(), "Dummy", "DummyOther"); 376 | this.keySchema = keySchema; 377 | } 378 | 379 | @Override 380 | protected List getTableSchema(String tableName) throws ResourceNotFoundException { 381 | return keySchema; 382 | } 383 | } 384 | } 385 | 386 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/transactions/TransactionDynamoDBFacadeTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014-2014 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.nio.ByteBuffer; 8 | import java.util.Collections; 9 | import java.util.Map; 10 | 11 | import org.junit.Test; 12 | 13 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 14 | import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException; 15 | import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue; 16 | 17 | public class TransactionDynamoDBFacadeTest { 18 | 19 | @Test 20 | public void testCheckExpectedStringValueWithMatchingItem() { 21 | Map item = Collections.singletonMap("Foo", new AttributeValue("Bar")); 22 | Map expected = Collections.singletonMap("Foo", new ExpectedAttributeValue(new AttributeValue("Bar"))); 23 | 24 | TransactionDynamoDBFacade.checkExpectedValues(expected, item); 25 | // no exception expected 26 | } 27 | 28 | @Test(expected = ConditionalCheckFailedException.class) 29 | public void testCheckExpectedStringValueWithNonMatchingItem() { 30 | Map item = Collections.singletonMap("Foo", new AttributeValue("Bar")); 31 | Map expected = Collections.singletonMap("Foo", new ExpectedAttributeValue(new AttributeValue("NotBar"))); 32 | 33 | TransactionDynamoDBFacade.checkExpectedValues(expected, item); 34 | } 35 | 36 | @Test 37 | public void testCheckExpectedBinaryValueWithMatchingItem() { 38 | Map item = Collections.singletonMap("Foo", new AttributeValue().withB(ByteBuffer.wrap(new byte[] { 1, 127, -127 }))); 39 | Map expected = Collections.singletonMap("Foo", new ExpectedAttributeValue(new AttributeValue().withB(ByteBuffer.wrap(new byte[] { 1, 127, -127 })))); 40 | 41 | TransactionDynamoDBFacade.checkExpectedValues(expected, item); 42 | // no exception expected 43 | } 44 | 45 | @Test(expected = ConditionalCheckFailedException.class) 46 | public void testCheckExpectedBinaryValueWithNonMatchingItem() { 47 | Map item = Collections.singletonMap("Foo", new AttributeValue().withB(ByteBuffer.wrap(new byte[] { 1, 127, -127 }))); 48 | Map expected = Collections.singletonMap("Foo", new ExpectedAttributeValue(new AttributeValue().withB(ByteBuffer.wrap(new byte[] { 0, 127, -127 })))); 49 | 50 | TransactionDynamoDBFacade.checkExpectedValues(expected, item); 51 | } 52 | 53 | @Test 54 | public void testCheckExpectedNumericValueWithMatchingItem() { 55 | Map item = Collections.singletonMap("Foo", new AttributeValue().withN("3.14")); 56 | Map expected = Collections.singletonMap("Foo", new ExpectedAttributeValue(new AttributeValue().withN("3.14"))); 57 | 58 | TransactionDynamoDBFacade.checkExpectedValues(expected, item); 59 | // no exception expected 60 | } 61 | 62 | @Test 63 | public void testCheckExpectedNumericValueWithMatchingNotStringEqualItem() { 64 | Map item = Collections.singletonMap("Foo", new AttributeValue().withN("3.140")); 65 | Map expected = Collections.singletonMap("Foo", new ExpectedAttributeValue(new AttributeValue().withN("3.14"))); 66 | 67 | TransactionDynamoDBFacade.checkExpectedValues(expected, item); 68 | // no exception expected 69 | } 70 | 71 | @Test(expected = ConditionalCheckFailedException.class) 72 | public void testCheckExpectedNumericValueWithNonMatchingItem() { 73 | Map item = Collections.singletonMap("Foo", new AttributeValue().withN("3.14")); 74 | Map expected = Collections.singletonMap("Foo", new ExpectedAttributeValue(new AttributeValue().withN("12"))); 75 | 76 | TransactionDynamoDBFacade.checkExpectedValues(expected, item); 77 | } 78 | 79 | @Test(expected = ConditionalCheckFailedException.class) 80 | public void testCheckExpectedNumericValueWithStringTypedItem() { 81 | Map item = Collections.singletonMap("Foo", new AttributeValue("3.14")); 82 | Map expected = Collections.singletonMap("Foo", new ExpectedAttributeValue(new AttributeValue().withN("3.14"))); 83 | 84 | TransactionDynamoDBFacade.checkExpectedValues(expected, item); 85 | } 86 | 87 | @Test(expected = IllegalArgumentException.class) 88 | public void testCheckExpectedInvalidNumericValue() { 89 | Map item = Collections.singletonMap("Foo", new AttributeValue().withN("1.1")); 90 | Map expected = Collections.singletonMap("Foo", new ExpectedAttributeValue(new AttributeValue().withN("!!.!!"))); 91 | 92 | TransactionDynamoDBFacade.checkExpectedValues(expected, item); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/util/ImmutableAttributeValueTest.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.util; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | import static org.junit.Assert.assertFalse; 9 | 10 | import java.nio.ByteBuffer; 11 | 12 | import org.junit.Test; 13 | 14 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 15 | import com.amazonaws.services.dynamodbv2.util.ImmutableAttributeValue; 16 | 17 | public class ImmutableAttributeValueTest { 18 | 19 | @Test 20 | public void testBSEquals() { 21 | byte[] b1 = { (byte)0x01 }; 22 | byte[] b2 = { (byte)0x01 }; 23 | AttributeValue av1 = new AttributeValue().withBS(ByteBuffer.wrap(b1)); 24 | AttributeValue av2 = new AttributeValue().withBS(ByteBuffer.wrap(b2)); 25 | ImmutableAttributeValue iav1 = new ImmutableAttributeValue(av1); 26 | ImmutableAttributeValue iav2 = new ImmutableAttributeValue(av2); 27 | assertEquals(iav1, iav2); 28 | } 29 | 30 | @Test 31 | public void testBSNotEq() { 32 | byte[] b1 = { (byte)0x01 }; 33 | byte[] b2 = { (byte)0x02 }; 34 | AttributeValue av1 = new AttributeValue().withBS(ByteBuffer.wrap(b1)); 35 | AttributeValue av2 = new AttributeValue().withBS(ByteBuffer.wrap(b2)); 36 | ImmutableAttributeValue iav1 = new ImmutableAttributeValue(av1); 37 | ImmutableAttributeValue iav2 = new ImmutableAttributeValue(av2); 38 | assertFalse(iav1.equals(iav2)); 39 | } 40 | 41 | @Test 42 | public void testBSWithNull() { 43 | byte[] b1 = { (byte)0x01 }; 44 | byte[] b2 = { (byte)0x01 }; 45 | AttributeValue av1 = new AttributeValue().withBS(ByteBuffer.wrap(b1), ByteBuffer.wrap(b1)); 46 | AttributeValue av2 = new AttributeValue().withBS(ByteBuffer.wrap(b2), null); 47 | ImmutableAttributeValue iav1 = new ImmutableAttributeValue(av1); 48 | ImmutableAttributeValue iav2 = new ImmutableAttributeValue(av2); 49 | assertFalse(iav1.equals(iav2)); 50 | } 51 | 52 | } 53 | --------------------------------------------------------------------------------