├── .github └── workflows │ ├── continuous-integration.yml │ ├── golang-tests.yml │ ├── publish-java-package.yml │ └── python-tests.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cpp ├── AlternatorClient.h ├── README.md └── examples │ ├── CMakeLists.txt │ └── demo.cpp ├── csharp ├── AlternatorLiveNodes.cs ├── EndpointProvider.cs ├── ScyllaDB.Alternator.csproj ├── ScyllaDB.Alternator.sln └── Test │ ├── ScyllaDB.Alternator.Test.csproj │ └── Test.cs ├── dns ├── README.md ├── dns-loadbalancer-rr.py └── dns-loadbalancer.py ├── go ├── .golangci.yml ├── Makefile ├── README.md ├── common │ ├── cert_source.go │ ├── config.go │ ├── go.mod │ ├── go.sum │ ├── header_whitelist.go │ ├── live_nodes.go │ └── utils.go ├── test │ ├── docker-compose.yml │ └── scylla │ │ └── scylla.yaml ├── v1 │ ├── README.md │ ├── alternator_lb.go │ ├── alternator_lb_test.go │ ├── go.mod │ └── go.sum └── v2 │ ├── README.md │ ├── alternator_lb.go │ ├── alternator_lb_test.go │ ├── go.mod │ └── go.sum ├── java ├── Makefile ├── README.md ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── scylladb │ │ └── alternator │ │ ├── AlternatorEndpointProvider.java │ │ ├── AlternatorLiveNodes.java │ │ └── AlternatorRequestHandler.java │ └── test │ └── java │ └── com │ └── scylladb │ └── alternator │ └── test │ ├── Demo1.java │ ├── Demo2.java │ └── Demo3.java ├── javascript ├── Alternator.js ├── MoviesCreateTable.js ├── MoviesItemOps01.js ├── MoviesItemOps02.js ├── README.md └── run_example.sh ├── python ├── Makefile ├── README.md ├── alternator_lb.py ├── docker │ ├── docker-compose.yml │ └── scylla │ │ └── scylla.yaml ├── pylintrc ├── requirement-test.txt ├── test_integration_boto3.py └── test_unit.py └── renovate.json /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'java/*' 8 | pull_request: 9 | paths: 10 | - 'java/*' 11 | 12 | jobs: 13 | compile: 14 | name: Compile Java Module 15 | runs-on: ubuntu-20.04 16 | defaults: 17 | run: 18 | working-directory: ./java 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-java@v4 22 | with: 23 | distribution: temurin 24 | java-version: 8 25 | cache: maven 26 | server-id: ossrh 27 | server-username: MAVEN_USERNAME 28 | server-password: MAVEN_CENTRAL_TOKEN 29 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 30 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 31 | 32 | - name: Compile Tests 33 | run: make compile compile-test 34 | 35 | - name: Validate 36 | run: make verify 37 | -------------------------------------------------------------------------------- /.github/workflows/golang-tests.yml: -------------------------------------------------------------------------------- 1 | name: 'GoLang - Tests' 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths: 7 | - go/** 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - go/** 12 | workflow_dispatch: 13 | 14 | defaults: 15 | run: 16 | working-directory: ./go 17 | jobs: 18 | test: 19 | name: GoLang - Tests 20 | runs-on: ubuntu-latest 21 | timeout-minutes: 10 22 | 23 | steps: 24 | - name: Checkout source 25 | uses: actions/checkout@v4 26 | 27 | - name: Run build 28 | run: | 29 | make build 30 | 31 | - name: Run linters 32 | run: | 33 | make check 34 | 35 | - name: Run unit tests 36 | run: | 37 | make unit-test 38 | 39 | - name: Run integration tests 40 | run: | 41 | make integration-test 42 | 43 | - name: Stop the cluster 44 | if: ${{ always() }} 45 | run: | 46 | make scylla-down 47 | -------------------------------------------------------------------------------- /.github/workflows/publish-java-package.yml: -------------------------------------------------------------------------------- 1 | name: Publish the Java Package to Maven Central 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | releaseVersion: 6 | description: 'Release version (e.g. "2.1.5")' 7 | required: true 8 | type: string 9 | 10 | env: 11 | IS_CICD: 1 12 | 13 | jobs: 14 | compile: 15 | name: Publish the Java Package to Maven Central 16 | runs-on: ubuntu-latest 17 | defaults: 18 | run: 19 | working-directory: ./java 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-java@v4 23 | with: 24 | distribution: temurin 25 | java-version: 8 26 | cache: maven 27 | server-id: ossrh 28 | server-username: MAVEN_USERNAME 29 | server-password: MAVEN_CENTRAL_TOKEN 30 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 31 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 32 | - name: Publish 33 | run: | 34 | mvn versions:set -DnewVersion=${{ inputs.releaseVersion }} 35 | mvn clean deploy -Prelease 36 | env: 37 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 38 | MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} 39 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} 40 | -------------------------------------------------------------------------------- /.github/workflows/python-tests.yml: -------------------------------------------------------------------------------- 1 | name: 'Python - Tests' 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths: 7 | - python/** 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - python/** 12 | workflow_dispatch: 13 | 14 | defaults: 15 | run: 16 | working-directory: ./python 17 | jobs: 18 | test: 19 | name: Python - Tests 20 | runs-on: ubuntu-latest 21 | timeout-minutes: 10 22 | 23 | steps: 24 | - name: Checkout source 25 | uses: actions/checkout@v4 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: '3.10' 31 | 32 | - name: Run linters 33 | run: | 34 | make check 35 | 36 | - name: Run unit tests 37 | run: | 38 | make unit-test 39 | 40 | - name: Run integration tests 41 | run: | 42 | make integration-test 43 | 44 | - name: Stop the cluster 45 | if: ${{ always() }} 46 | run: | 47 | make scylla-kill 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | **/.idea 3 | **/__pycache__ 4 | **/.venv 5 | java/target 6 | 7 | csharp/**/*.suo 8 | csharp/**/*.user 9 | csharp/**/*.sln.docstates 10 | csharp/**/[bB]in 11 | csharp/**/[oO]bj 12 | csharp/**/release 13 | csharp/**/*.nupkg 14 | csharp/**/packages/* 15 | csharp/**/[Tt]est[Rr]esult*/ 16 | csharp/**/[Bb]uild[Ll]og.* 17 | 18 | go/bin/ 19 | go/test/scylla/db.crt 20 | go/test/scylla/db.key 21 | 22 | python/bin/ 23 | python/docker/scylla/db.crt 24 | python/docker/scylla/db.key 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Alternator Load Balancing 2 | 3 | First off, thanks for taking the time to contribute! 4 | 5 | ## Java package 6 | 7 | ### Publish a new release to Maven Central 8 | 9 | On GitHub, go to the [publish Java package workflow](https://github.com/scylladb/alternator-load-balancing/actions/workflows/publish-java-package.yml) and click “Run workflow”. Enter the release version (e.g. “1.2.3”) and confirm. 10 | 11 | After the release, bump the `project.properties.revision` property in the `pom.xml` and add the suffix `-SNAPSHOT`. For instance, after the release of version `1.2.3`, set the revision to `1.2.4-SNAPSHOT`. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alternator Load Balancing 2 | 3 | This repository contains a collection of source code and scripts that can 4 | be used to implement **load balancing** for a Scylla Alternator cluster. 5 | 6 | All code in this repository is open source, licensed under the 7 | [Apache License 2.0](LICENSE). 8 | 9 | ## Introduction 10 | 11 | **[Scylla](https://github.com/scylladb/scylla)** is an open-source distributed 12 | database. **[Alternator](https://docs.scylladb.com/using-scylla/alternator/)** 13 | is a Scylla feature which adds Amazon DynamoDB™ compatibility to 14 | Scylla. With Alternator, Scylla is fully (or [almost fully](https://github.com/scylladb/scylla/blob/master/docs/alternator/compatibility.md)) 15 | compatible with DynamoDB's HTTP and JSON based API. Unmodified applications 16 | written with any of Amazon's [SDK libraries](https://aws.amazon.com/tools/) 17 | can connect to a Scylla Alternator cluster instead of to Amazon's DynamoDB. 18 | 19 | However, there is still one fundamental difference between how DynamoDB 20 | and a Scylla cluster appear to an application: 21 | 22 | - The entire DynamoDB service is presented to the application as a 23 | **single endpoint**, for example 24 | `http://dynamodb.us-east-1.amazonaws.com`. 25 | - Scylla is not a single endpoint - it is a _distributed_ database - a 26 | cluster of **many nodes**. 27 | 28 | If we configure the application to use just one of the Scylla nodes as the 29 | single endpoint, this specific node will become a performance bottleneck 30 | as it gets more work than the other nodes. Moreover, this node will become 31 | a single point of failure - if it fails, the entire service is unavailable. 32 | 33 | So what Alternator users need now is a way for a DynamoDB application - which 34 | was written with just a single endpoint in mind - to send requests to all of 35 | Alternator's nodes, not just to one. The mechanisms we are looking for should 36 | equally load all of Alternator's nodes (_load balancing_) and ensure that the 37 | service continues normally even if some of these nodes go down (_high 38 | availability_). 39 | 40 | In our blog post [Load Balancing in Scylla Alternator](https://www.scylladb.com/2021/04/13/load-balancing-in-scylla-alternator/) 41 | we explained in more detail the need for load balancing in Alternator and the 42 | various server-side and client-side options that are available. 43 | 44 | The goal of this repository is to offer Alternator users with such 45 | load balancing mechanisms, in the form of code examples, libraries, 46 | and documentation. 47 | 48 | ## This repository 49 | 50 | The most obvious load-balancing solution is a _load balancer_, a machine 51 | or a virtual service which sits in front of the Alternator cluster and 52 | forwards the HTTP requests that it gets to the different Alternator nodes. 53 | This is a good option for some setups, but a costly one because all the 54 | request traffic needs to flow through the load balancer. 55 | 56 | In [this document](https://docs.google.com/document/d/1twgrs6IM1B10BswMBUNqm7bwu5HCm47LOYE-Hdhuu_8/) we surveyed some additional **server-side** 57 | load-balancing mechanisms besides the TCP or HTTP load balancer. 58 | These including _DNS_, _virtual IP addresses_, and _coordinator-only nodes_. 59 | In the [dns](dns) subdirectory in this repository we demonstrate a simple 60 | proof-of-concept of the DNS mechanism. 61 | 62 | But the bulk of this repository is devoted to **client-side** load balancing. 63 | In client-side load balancing, the client is modified to connect to all 64 | Alternator nodes instead of just one. Client-side load balancing simplifies 65 | server deployment and lowers server costs - as we do not need to deploy 66 | additional server-side nodes or services. 67 | 68 | Of course, our goal is to require _as little as possible_ changes to the 69 | client. Ideally, all that would need to be changed in an application is to 70 | have it load an additional library, or initialize the existing library a bit 71 | differently; From there on, the usual unmodified AWS SDK functions will 72 | automatically use all of Alternator's nodes instead of just one. 73 | 74 | We currently provide libraries to do exactly that in five programming 75 | languages: [go (AWS SDK v1)](go/v1), [go (AWS SDK v2)](go/v2), [java](java), [javascript](javascript) (node.js), 76 | [python](python) and [C++](cpp). Each of these directories includes a README 77 | file explaining how to use this library in an application. These libraries are 78 | not complete DynamoDB drivers - the application continues to use Amazon's 79 | SDKs (e.g., boto3 in Python). Rather, what our libraries do is to 80 | automatically retrieve the list of nodes in an Alternator cluster, and 81 | configure or trick the Amazon SDK into sending requests to many different 82 | nodes instead of always to the same one. 83 | -------------------------------------------------------------------------------- /cpp/AlternatorClient.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class AlternatorClient : public Aws::DynamoDB::DynamoDBClient { 9 | protected: 10 | Aws::String _protocol; 11 | Aws::String _port; 12 | mutable std::vector _nodes; 13 | mutable std::mutex _nodes_mutex; 14 | mutable size_t _node_idx = 0; 15 | mutable size_t _updater_idx = 0; 16 | std::unique_ptr _node_updater; 17 | std::atomic _keep_updating; 18 | public: 19 | AlternatorClient(Aws::String protocol, Aws::String control_addr, Aws::String port, 20 | const Aws::Client::ClientConfiguration &clientConfiguration = Aws::Client::ClientConfiguration()) 21 | : Aws::DynamoDB::DynamoDBClient(clientConfiguration) 22 | , _protocol(protocol) 23 | , _port(port) { 24 | Aws::Http::URI initial_node(protocol + "://" + control_addr + ":" + port); 25 | _nodes.push_back(std::move(initial_node)); 26 | FetchLocalNodes(); 27 | } 28 | 29 | virtual ~AlternatorClient() { 30 | _keep_updating = false; 31 | if (_node_updater) { 32 | _node_updater->join(); 33 | } 34 | } 35 | 36 | virtual void BuildHttpRequest(const Aws::AmazonWebServiceRequest &request, const std::shared_ptr< Aws::Http::HttpRequest > &httpRequest) const override { 37 | Aws::Http::URI next = NextNode(); 38 | httpRequest->GetUri() = std::move(next); 39 | return Aws::DynamoDB::DynamoDBClient::BuildHttpRequest(request, httpRequest); 40 | } 41 | 42 | void FetchLocalNodes() { 43 | Aws::Http::URI uri; 44 | { 45 | std::lock_guard guard(_nodes_mutex); 46 | assert(!_nodes.empty()); 47 | uri = GetURIForUpdates(); 48 | } 49 | std::shared_ptr request(new Aws::Http::Standard::StandardHttpRequest(uri, Aws::Http::HttpMethod::HTTP_GET)); 50 | request->SetResponseStreamFactory([] { return new std::stringstream; }); 51 | std::shared_ptr response = MakeHttpRequest(request); 52 | Aws::Utils::Json::JsonValue json_raw = response->GetResponseBody(); 53 | Aws::Utils::Json::JsonView json = json_raw.View(); 54 | if (json.IsListType()) { 55 | std::vector nodes; 56 | Aws::Utils::Array endpoints = json.AsArray(); 57 | Aws::Utils::Json::JsonView* raw_endpoints = endpoints.GetUnderlyingData(); 58 | for (size_t i = 0; i < endpoints.GetLength(); ++i) { 59 | const Aws::Utils::Json::JsonView& element = raw_endpoints[i]; 60 | if (element.IsString()) { 61 | nodes.push_back(Aws::Http::URI(_protocol + "://" + element.AsString() + ":" + _port)); 62 | } 63 | } 64 | if (!nodes.empty()) { 65 | std::lock_guard guard(_nodes_mutex); 66 | _nodes = std::move(nodes); 67 | _node_idx = 0; 68 | } 69 | } else { 70 | throw std::runtime_error("Failed to fetch the list of live nodes"); 71 | } 72 | } 73 | 74 | Aws::Http::URI NextNode() const { 75 | std::lock_guard guard(_nodes_mutex); 76 | assert(!_nodes.empty()); 77 | size_t idx = _node_idx; 78 | _node_idx = (_node_idx + 1) % _nodes.size(); 79 | return _nodes[idx]; 80 | } 81 | 82 | template 83 | void StartNodeUpdater(Duration duration) { 84 | _keep_updating = true; 85 | _node_updater = std::unique_ptr(new std::thread([this, duration] { 86 | while (_keep_updating) { 87 | try { 88 | FetchLocalNodes(); 89 | std::this_thread::sleep_for(duration); 90 | } catch (...) { 91 | // continue the thread anyway until it's explicitly stopped 92 | } 93 | } 94 | })); 95 | } 96 | 97 | protected: 98 | // GetURIForUpdates() assumes that no concurrent updates to _nodes are performed. 99 | // _nodes should be locked with _nodes_mutex prior to calling this function. 100 | Aws::Http::URI GetURIForUpdates() const { 101 | assert(!_nodes_mutex.try_lock()); 102 | size_t idx = _updater_idx; 103 | _updater_idx = (_updater_idx + 1) % _nodes.size(); 104 | Aws::Http::URI ret = _nodes[idx]; 105 | ret.SetPath(ret.GetPath() + "/localnodes"); 106 | return ret; 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /cpp/README.md: -------------------------------------------------------------------------------- 1 | # Alternator Load Balancing: C++ 2 | 3 | This directory contains a load-balancing wrapper for the C++ driver. 4 | 5 | ## Contents 6 | 7 | Alternator load balancing wrapper consists of a single header: `AlternatorClient.h`. With this header included, it's possible to use `AlternatorClient` class as a drop-in replacement for stock `DynamoDBClient`, which will provide a client-side load balancing layer for the Alternator cluster. 8 | 9 | ## Usage 10 | 11 | In order to switch from the stock driver to Alternator load balancing, it's enough to replace the initialization code in the existing application: 12 | ```cpp 13 | Aws::DynamoDB::DynamoDBClient dynamoClient(clientConfig); 14 | ``` 15 | to 16 | ```cpp 17 | AlternatorClient dynamoClient("http", "localhost", "8000", clientConfig); 18 | dynamoClient.StartNodeUpdater(std::chrono::seconds(1)); 19 | ``` 20 | After that single change, all requests sent via the `dynamoClient` instance of `DynamoDBClient` will be implicitly routed to Alternator nodes. 21 | Parameters accepted by the Alternator client are: 22 | 1. `protocol`: `http` or `https`, used for client-server communication 23 | 2. `addr`: hostname of one of the Alternator nodes, which should be contacted to retrieve cluster topology information 24 | 3. `port`: port of the Alternator nodes - each node is expected to use the same port number 25 | 26 | Running an update thread (`dynamoClient.StartNodeUpdater(std::chrono::seconds(1))`) is optional, but is highly recommended due to possible topology changes in a live cluster - the active node list can change in time. The update thread accepts a single argument, which describes how often the node list is updated. 27 | 28 | ## Details 29 | 30 | Alternator load balancing for C++ works by providing a thin layer which distributes the requests to different Alternator nodes. Initially, the driver contacts one of the Alternator nodes and retrieves the list of active nodes which can be use to accept user requests. This list can be perodically refreshed in order to ensure that any topology changes are taken into account. Once a client sends a request, the load balancing layer picks one of the active Alternator nodes as the target. Currently, nodes are picked in a round-robin fashion. 31 | 32 | ## Example 33 | 34 | An example program can be found in the `examples` directory. The program tries to connect to an alternator cluster and then: 35 | * creates a table 36 | * fills the table with 5 example items 37 | * scans the table to verify that the items were properly inserted 38 | 39 | The demo can be compiled via CMake: 40 | ```bash 41 | cd examples 42 | mkdir build 43 | cmake .. 44 | ``` 45 | 46 | By default, the demo program tries to connect to localhost, via http, on port 8000. Please edit the source code to provide your own endpoint if necessary. 47 | 48 | In order to run the demo, just pass the table name as the first argument: 49 | ```bash 50 | ./demo test_table1 51 | ``` 52 | -------------------------------------------------------------------------------- /cpp/examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | project(alternator-example) 3 | set (CMAKE_CXX_STANDARD 11) 4 | set (BUILD_SHARED_LIBS ON) 5 | 6 | find_package(AWSSDK REQUIRED COMPONENTS dynamodb) 7 | 8 | add_executable(demo demo.cpp) 9 | target_link_libraries(demo ${AWSSDK_LINK_LIBRARIES}) 10 | 11 | -------------------------------------------------------------------------------- /cpp/examples/demo.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "../AlternatorClient.h" 15 | 16 | int main(int argc, char** argv) 17 | { 18 | if (argc < 2) { 19 | std::cout << "This demo program creates a table, fills it with example data " 20 | "and then reads it. Each request is subject to load balancing and will " 21 | "be sent to different alternator nodes, as long as multiple nodes are available." << std::endl; 22 | std::cout << "Usage: " << argv[0] << " " << std::endl; 23 | return 1; 24 | } 25 | 26 | Aws::SDKOptions options; 27 | Aws::InitAPI(options); 28 | 29 | const Aws::String table(argv[1]); 30 | 31 | Aws::Client::ClientConfiguration config; 32 | config.verifySSL = false; 33 | config.retryStrategy = std::shared_ptr(new Aws::Client::DefaultRetryStrategy(3, 10)); 34 | AlternatorClient alternator("http", "localhost", "8000", config); 35 | alternator.StartNodeUpdater(std::chrono::seconds(1)); 36 | 37 | Aws::DynamoDB::Model::CreateTableRequest create_req; 38 | 39 | // Create a new table 40 | create_req.SetTableName(table); 41 | create_req.SetBillingMode(Aws::DynamoDB::Model::BillingMode::PAY_PER_REQUEST); 42 | create_req.AddAttributeDefinitions( 43 | Aws::DynamoDB::Model::AttributeDefinition().WithAttributeName("id").WithAttributeType(Aws::DynamoDB::Model::ScalarAttributeType::S) 44 | ); 45 | create_req.AddKeySchema( 46 | Aws::DynamoDB::Model::KeySchemaElement().WithAttributeName("id").WithKeyType(Aws::DynamoDB::Model::KeyType::HASH) 47 | ); 48 | 49 | std::cout << "Creating table " << table << std::endl; 50 | const Aws::DynamoDB::Model::CreateTableOutcome& create_result = alternator.CreateTable(create_req); 51 | if (create_result.IsSuccess()) { 52 | std::cout << "Table created:" << std::endl; 53 | std::cout << create_result.GetResult().GetTableDescription().Jsonize().View().WriteReadable() << std::endl; 54 | } else { 55 | std::cout << "Failed to create table: " << create_result.GetError().GetMessage() << std::endl; 56 | return 1; 57 | } 58 | 59 | std::cout << "Filling table " << table << " with data" << std::endl; 60 | for (int p = 0; p < 5; ++p) { 61 | Aws::DynamoDB::Model::PutItemRequest put_req; 62 | put_req.SetTableName(table); 63 | 64 | put_req.AddItem("id", Aws::DynamoDB::Model::AttributeValue().SetS(("item" + std::to_string(p)).c_str())); 65 | 66 | for (int i = 0; i < 3; ++i) { 67 | const Aws::String attr = ("attr" + std::to_string(i)).c_str(); 68 | const Aws::String val = ("val" + std::to_string(i)).c_str(); 69 | put_req.AddItem(attr, Aws::DynamoDB::Model::AttributeValue().SetS(val)); 70 | } 71 | 72 | const Aws::DynamoDB::Model::PutItemOutcome put_result = alternator.PutItem(put_req); 73 | if (!put_result.IsSuccess()) { 74 | std::cout << put_result.GetError().GetMessage() << std::endl; 75 | return 1; 76 | } 77 | } 78 | 79 | std::cout << "Scanning table " << table << std::endl; 80 | Aws::DynamoDB::Model::ScanRequest scan_req; 81 | scan_req.SetTableName(table); 82 | 83 | const Aws::DynamoDB::Model::ScanOutcome& scan_result = alternator.Scan(scan_req); 84 | if (scan_result.IsSuccess()) { 85 | std::cout << "Scan results:" << std::endl; 86 | const Aws::Vector>& items = scan_result.GetResult().GetItems(); 87 | for (const auto& item : items) { 88 | std::cout << "Item: " << std::endl; 89 | for (const auto& attr : item) { 90 | std::cout << '\t' << attr.first << ":\t" << attr.second.Jsonize().View().WriteCompact() << std::endl; 91 | } 92 | } 93 | } else { 94 | std::cout << "Failed to scan table: " << scan_result.GetError().GetMessage() << std::endl; 95 | } 96 | 97 | Aws::ShutdownAPI(options); 98 | } 99 | -------------------------------------------------------------------------------- /csharp/AlternatorLiveNodes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using System.Threading.Tasks; 10 | using System.Diagnostics; 11 | using Amazon.Runtime.Endpoints; 12 | 13 | namespace ScyllaDB.Alternator 14 | { 15 | public class AlternatorLiveNodes 16 | { 17 | private readonly string _alternatorScheme; 18 | private readonly int _alternatorPort; 19 | private List _liveNodes; 20 | private readonly ReaderWriterLockSlim _liveNodesLock = new(); 21 | private readonly List _initialNodes; 22 | private int _nextLiveNodeIndex; 23 | private readonly string _rack; 24 | private readonly string _datacenter; 25 | private bool _started; 26 | 27 | private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 28 | 29 | public AlternatorLiveNodes(Uri liveNode, string datacenter, string rack) 30 | : this([liveNode], liveNode.Scheme, liveNode.Port, datacenter, rack) 31 | { 32 | } 33 | 34 | public AlternatorLiveNodes(List nodes, string scheme, int port, string datacenter, string rack) 35 | { 36 | if (nodes == null || nodes.Count == 0) 37 | { 38 | throw new SystemException("liveNodes cannot be null or empty"); 39 | } 40 | 41 | _initialNodes = nodes; 42 | _alternatorScheme = scheme; 43 | _alternatorPort = port; 44 | _rack = rack; 45 | _datacenter = datacenter; 46 | _liveNodes = new List(); 47 | foreach (var node in _initialNodes) 48 | { 49 | _liveNodes.Add(node); 50 | } 51 | } 52 | 53 | public Task Start(CancellationToken cancellationToken) 54 | { 55 | if (_started) 56 | { 57 | return Task.CompletedTask; 58 | } 59 | 60 | Validate(); 61 | 62 | Task.Run(() => 63 | { 64 | UpdateCycle(cancellationToken); 65 | return Task.CompletedTask; 66 | }, cancellationToken); 67 | _started = true; 68 | return Task.CompletedTask; 69 | } 70 | 71 | private void UpdateCycle(CancellationToken cancellationToken) 72 | { 73 | Logger.Debug("AlternatorLiveNodes thread started"); 74 | try 75 | { 76 | while (true) 77 | { 78 | if (cancellationToken.IsCancellationRequested) 79 | { 80 | return; 81 | } 82 | 83 | try 84 | { 85 | UpdateLiveNodes(); 86 | } 87 | catch (IOException e) 88 | { 89 | Logger.Error(e, "AlternatorLiveNodes failed to sync nodes list: %"); 90 | } 91 | 92 | try 93 | { 94 | Thread.Sleep(1000); 95 | } 96 | catch (ThreadInterruptedException e) 97 | { 98 | Logger.Info("AlternatorLiveNodes thread interrupted and stopping"); 99 | return; 100 | } 101 | } 102 | } 103 | finally 104 | { 105 | Logger.Info("AlternatorLiveNodes thread stopped"); 106 | } 107 | } 108 | 109 | public class ValidationError : Exception 110 | { 111 | public ValidationError(string message) : base(message) 112 | { 113 | } 114 | 115 | public ValidationError(string message, Exception cause) : base(message, cause) 116 | { 117 | } 118 | } 119 | 120 | public void Validate() 121 | { 122 | try 123 | { 124 | // Make sure that `alternatorScheme` and `alternatorPort` are correct values 125 | HostToUri("1.1.1.1"); 126 | } 127 | catch (UriFormatException e) 128 | { 129 | throw new ValidationError("failed to validate configuration", e); 130 | } 131 | } 132 | 133 | private Uri HostToUri(string host) 134 | { 135 | return new Uri($"{_alternatorScheme}://{host}:{_alternatorPort}"); 136 | } 137 | 138 | private List getLiveNodes() 139 | { 140 | _liveNodesLock.EnterReadLock(); 141 | try 142 | { 143 | return _liveNodes.ToList(); 144 | } 145 | finally 146 | { 147 | _liveNodesLock.ExitReadLock(); 148 | } 149 | } 150 | 151 | private void setLiveNodes(List nodes) 152 | { 153 | _liveNodesLock.EnterWriteLock(); 154 | _liveNodes = nodes; 155 | _liveNodesLock.ExitWriteLock(); 156 | } 157 | 158 | public Uri NextAsUri() 159 | { 160 | var nodes = getLiveNodes(); 161 | if (nodes.Count == 0) 162 | { 163 | throw new InvalidOperationException("No live nodes available"); 164 | } 165 | 166 | return nodes[Math.Abs(Interlocked.Increment(ref _nextLiveNodeIndex) % nodes.Count)]; 167 | } 168 | 169 | private Uri NextAsUri(string path, string query) 170 | { 171 | Uri uri = NextAsUri(); 172 | return new Uri($"{uri.Scheme}://{uri.Host}:{uri.Port}{path}?{query}"); 173 | } 174 | 175 | private static string StreamToString(Stream stream) 176 | { 177 | using var reader = new StreamReader(stream); 178 | return reader.ReadToEnd(); 179 | } 180 | 181 | private void UpdateLiveNodes() 182 | { 183 | var newHosts = GetNodes(NextAsLocalNodesUri()); 184 | if (newHosts.Count == 0) return; 185 | setLiveNodes(newHosts); 186 | Logger.Info($"Updated hosts to {_liveNodes}"); 187 | } 188 | 189 | private List GetNodes(Uri uri) 190 | { 191 | using var client = new HttpClient(); 192 | var response = client.GetAsync(uri).Result; 193 | if (!response.IsSuccessStatusCode) 194 | { 195 | return []; 196 | } 197 | 198 | var responseBody = StreamToString(response.Content.ReadAsStreamAsync().Result); 199 | // response looks like: ["127.0.0.2","127.0.0.3","127.0.0.1"] 200 | responseBody = responseBody.Trim(); 201 | responseBody = responseBody.Substring(1, responseBody.Length - 2); 202 | var list = responseBody.Split(','); 203 | var newHosts = new List(); 204 | foreach (var host in list) 205 | { 206 | if (string.IsNullOrEmpty(host)) 207 | { 208 | continue; 209 | } 210 | 211 | var trimmedHost = host.Trim().Substring(1, host.Length - 2); 212 | try 213 | { 214 | newHosts.Add(HostToUri(trimmedHost)); 215 | } 216 | catch (UriFormatException e) 217 | { 218 | Logger.Error(e, $"Invalid host: {trimmedHost}"); 219 | } 220 | } 221 | 222 | return newHosts; 223 | } 224 | 225 | private Uri NextAsLocalNodesUri() 226 | { 227 | if (string.IsNullOrEmpty(_rack) && string.IsNullOrEmpty(_datacenter)) 228 | { 229 | return NextAsUri("/localnodes", null); 230 | } 231 | 232 | var query = ""; 233 | if (!string.IsNullOrEmpty(_rack)) 234 | { 235 | query = "rack=" + _rack; 236 | } 237 | 238 | if (string.IsNullOrEmpty(_datacenter)) return NextAsUri("/localnodes", query); 239 | if (string.IsNullOrEmpty(query)) 240 | { 241 | query = $"dc={_datacenter}"; 242 | } 243 | else 244 | { 245 | query += $"&dc={_datacenter}"; 246 | } 247 | 248 | return NextAsUri("/localnodes", query); 249 | } 250 | 251 | public class FailedToCheck : Exception 252 | { 253 | public FailedToCheck(string message, Exception cause) : base(message, cause) 254 | { 255 | } 256 | 257 | public FailedToCheck(string message) : base(message) 258 | { 259 | } 260 | } 261 | 262 | public void CheckIfRackAndDatacenterSetCorrectly() 263 | { 264 | if (string.IsNullOrEmpty(_rack) && string.IsNullOrEmpty(_datacenter)) 265 | { 266 | return; 267 | } 268 | 269 | try 270 | { 271 | var nodes = GetNodes(NextAsLocalNodesUri()); 272 | if (nodes.Count == 0) 273 | { 274 | throw new ValidationError("node returned empty list, datacenter or rack are set incorrectly"); 275 | } 276 | } 277 | catch (IOException e) 278 | { 279 | throw new FailedToCheck("failed to read list of nodes from the node", e); 280 | } 281 | } 282 | 283 | public bool CheckIfRackDatacenterFeatureIsSupported() 284 | { 285 | var uri = NextAsUri("/localnodes", null); 286 | Uri fakeRackUrl; 287 | try 288 | { 289 | fakeRackUrl = new Uri($"{uri.Scheme}://{uri.Host}:{uri.Port}{uri.Query}&rack=fakeRack"); 290 | } 291 | catch (UriFormatException e) 292 | { 293 | // Should not ever happen 294 | throw new FailedToCheck("Invalid Uri: " + uri, e); 295 | } 296 | 297 | try 298 | { 299 | var hostsWithFakeRack = GetNodes(fakeRackUrl); 300 | var hostsWithoutRack = GetNodes(uri); 301 | if (hostsWithoutRack.Count == 0) 302 | { 303 | // This should not normally happen. 304 | // If list of nodes is empty, it is impossible to conclude if it supports rack/datacenter filtering or not. 305 | throw new FailedToCheck($"host {uri} returned empty list"); 306 | } 307 | 308 | // When rack filtering is not supported server returns same nodes. 309 | return hostsWithFakeRack.Count != hostsWithoutRack.Count; 310 | } 311 | catch (IOException e) 312 | { 313 | throw new FailedToCheck("failed to read list of nodes from the node", e); 314 | } 315 | } 316 | } 317 | } -------------------------------------------------------------------------------- /csharp/EndpointProvider.cs: -------------------------------------------------------------------------------- 1 | using Amazon.Runtime.Endpoints; 2 | 3 | namespace ScyllaDB.Alternator 4 | { 5 | public class EndpointProvider : IEndpointProvider 6 | { 7 | private readonly AlternatorLiveNodes _liveNodes; 8 | private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 9 | 10 | public EndpointProvider(Uri seedUri, string datacenter, string rack) 11 | { 12 | _liveNodes = new AlternatorLiveNodes(seedUri, datacenter, rack); 13 | try 14 | { 15 | _liveNodes.Validate(); 16 | _liveNodes.CheckIfRackAndDatacenterSetCorrectly(); 17 | if (datacenter.Length != 0 || rack.Length != 0) 18 | { 19 | if (!_liveNodes.CheckIfRackDatacenterFeatureIsSupported()) 20 | { 21 | Logger.Error($"server {seedUri} does not support rack or datacenter filtering"); 22 | } 23 | } 24 | } 25 | catch (Exception e) 26 | { 27 | throw new SystemException("failed to start EndpointProvider", e); 28 | } 29 | 30 | _liveNodes.Start(CancellationToken.None); 31 | } 32 | 33 | public Endpoint ResolveEndpoint(EndpointParameters parameters) 34 | { 35 | return new Endpoint(_liveNodes.NextAsUri().ToString()); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /csharp/ScyllaDB.Alternator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | false 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ..\..\..\..\home\dmitry.kropachev\.nuget\packages\nunit\4.2.2\lib\net6.0\nunit.framework.dll 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /csharp/ScyllaDB.Alternator.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScyllaDB.Alternator", "ScyllaDB.Alternator.csproj", "{C8840517-C776-4765-8A2B-2989A66BE97D}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScyllaDB.Alternator.Test", "Test/ScyllaDB.Alternator.Test.csproj", "{7D4CA20F-3CC6-48B7-8C91-3A023F2FA834}" 6 | EndProject 7 | 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {C8840517-C776-4765-8A2B-2989A66BE97D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {C8840517-C776-4765-8A2B-2989A66BE97D}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {C8840517-C776-4765-8A2B-2989A66BE97D}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {C8840517-C776-4765-8A2B-2989A66BE97D}.Release|Any CPU.Build.0 = Release|Any CPU 18 | 19 | {7D4CA20F-3CC6-48B7-8C91-3A023F2FA834}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {7D4CA20F-3CC6-48B7-8C91-3A023F2FA834}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {7D4CA20F-3CC6-48B7-8C91-3A023F2FA834}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {7D4CA20F-3CC6-48B7-8C91-3A023F2FA834}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | EndGlobal 25 | -------------------------------------------------------------------------------- /csharp/Test/ScyllaDB.Alternator.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | latest 6 | enable 7 | enable 8 | false 9 | false 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /csharp/Test/Test.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using Amazon.DynamoDBv2.Model; 3 | using Amazon.Runtime; 4 | using System.Net; 5 | 6 | namespace ScyllaDB.Alternator 7 | { 8 | [TestFixture] 9 | public class Test 10 | { 11 | // And this is the Alternator-specific way to get a DynamoDB connection 12 | // which load-balances several Scylla nodes. 13 | private static AmazonDynamoDBClient GetAlternatorClient(Uri uri, AWSCredentials credentials, string datacenter, string rack) 14 | { 15 | var handler = new EndpointProvider(uri, datacenter, rack); 16 | var config = new AmazonDynamoDBConfig 17 | { 18 | RegionEndpoint = Amazon.RegionEndpoint.USEast1, // Region doesn't matter 19 | EndpointProvider = handler 20 | }; 21 | 22 | return new AmazonDynamoDBClient(credentials, config); 23 | } 24 | 25 | private readonly string _user = TestContext.Parameters.Get("User", "none"); 26 | private readonly string _password = TestContext.Parameters.Get("Password", "none"); 27 | private readonly string _endpoint = TestContext.Parameters.Get("Endpoint", "http://127.0.0.1:8080"); 28 | 29 | [Test] 30 | public async Task BasicTableTest([Values("","dc1")] string datacenter, [Values("", "rack1")]string rack) 31 | { 32 | DisableCertificateChecks(); 33 | 34 | 35 | var credentials = new BasicAWSCredentials(_user, _password); 36 | 37 | var ddb = GetAlternatorClient(new Uri(_endpoint), credentials, datacenter, rack); 38 | 39 | var rand = new Random(); 40 | string tabName = "table" + rand.Next(1000000); 41 | 42 | await ddb.CreateTableAsync(tabName, 43 | [ 44 | new("k", KeyType.HASH), 45 | new("c", KeyType.RANGE) 46 | ], 47 | [ 48 | new("k", ScalarAttributeType.N), 49 | new("c", ScalarAttributeType.N) 50 | ], 51 | new ProvisionedThroughput { ReadCapacityUnits = 1, WriteCapacityUnits = 1 }); 52 | 53 | // run ListTables several times 54 | for (int i = 0; i < 10; i++) 55 | { 56 | var tables = await ddb.ListTablesAsync(); 57 | Console.WriteLine(tables); 58 | } 59 | 60 | await ddb.DeleteTableAsync(tabName); 61 | ddb.Dispose(); 62 | } 63 | 64 | // A hack to disable SSL certificate checks. Useful when running with 65 | // a self-signed certificate. Shouldn't be used in production of course 66 | static void DisableCertificateChecks() 67 | { 68 | // For AWS SDK 69 | Environment.SetEnvironmentVariable("AWS_DISABLE_CERT_CHECKING", "true"); 70 | 71 | // For general HTTPS connections 72 | ServicePointManager.ServerCertificateValidationCallback = 73 | delegate 74 | { 75 | return true; 76 | }; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /dns/README.md: -------------------------------------------------------------------------------- 1 | # Alternator - DNS load balancing - proof-of-concept 2 | 3 | ## Introduction 4 | As explained in the [toplevel README](../README.md), DynamoDB clients 5 | are usually aware of a **single endpoint**, a single URL to which they 6 | connect - e.g., `http://dynamodb.us-east-1.amazonaws.com`. One of the 7 | approaches with which requests to a single URL can be directed many 8 | Scylla nodes is via a DNS server: We can configure a DNS server to return 9 | one of the live Scylla nodes (in the relevant data center). If we assume 10 | that there are many different clients, the overall load will be balanced. 11 | 12 | Scylla provides a simple HTTP request, "`/localnodes`", with which we 13 | can retrieve the current list of live nodes in this data center. 14 | 15 | ## dns-loadbalancer.py 16 | **Any** configurable DNS server can be used for this purpose. A script 17 | can periodically fetch the current list of live nodes by sending a 18 | `/localnodes` request to any previously-known live node. The script can 19 | then configure the DNS server to randomly return nodes from this list (or 20 | return random subsets from this list) for the chosen domain name. 21 | 22 | The Python program `dns-loadbalancer.py` is a self-contained example of 23 | this approach. It is not meant to be used for production workloads, but 24 | more as a *proof of concept*, of what can be done. The code uses the Python 25 | library **dnslib** to serve DNS requests on port 53 (both UDP and TCP). 26 | Requests to _any_ domain name are answered by one of the live Scylla nodes 27 | at random. The Python code also periodically (once every second) makes a 28 | `/localnodes` request to one of the known nodes to refresh the list of 29 | live nodes. 30 | 31 | To use `dns-loadbalancer.py`, run it on some machine, and set up an 32 | existing name server to point a specific domain name, e.g., 33 | `alternator.example.com` to this name server (i.e., an `NS` record). 34 | Now, whenever the application tries to resolve `alternator.example.com` 35 | our machine running `dns-loadbalancer.py` gets the request, and can 36 | respond with a random Scylla node. 37 | 38 | If your setup does not have any DNS server or domain name which you can 39 | control (as `alternator.example.com` in the above example), an alternative 40 | setup is to configure the client machine to use the demo DNS as its 41 | default DNS server. This means that the demo DNS server will receive **all** 42 | DNS requests, so the code needs to be changed (a bit) to only return random 43 | Scylla nodes for a specific "fake" domain (e.g., `alternator.example.com`, 44 | even if you don't control that domain), and pass on every other requests to 45 | a real name server. 46 | 47 | `dns-loadbalancer.py` should be edited to change the Alternator port 48 | number (which must be identical across the cluster) - in the `alternator_port` 49 | variable - and also an initial list of known Scylla nodes in the `livenodes` 50 | variable. As explained above, this list will be refreshed every one second, 51 | but this process must start by at least one known nodes, which we can 52 | contact to find the list of all the nodes. 53 | 54 | ### Example 55 | 56 | In the following example, Scylla is running on port 8000 on three 57 | IP addresses - 127.0.0.1, 127.0.0.2, and 127.0.0.3. The initial 58 | `livenodes` list contains just 127.0.0.1. 59 | 60 | ``` 61 | $ sudo ./dns-loadbalancer.py 62 | updating livenodes from http://127.0.0.1:8000/localnodes 63 | ['127.0.0.2', '127.0.0.3', '127.0.0.1'] 64 | ... 65 | ``` 66 | 67 | ``` 68 | $ dig @localhost alternator.example.com 69 | ... 70 | ;; ANSWER SECTION: 71 | alternator.example.com. 4 IN A 127.0.0.3 72 | $ dig @localhost alternator.example.com 73 | ... 74 | ;; ANSWER SECTION: 75 | alternator.example.com. 4 IN A 127.0.0.2 76 | ``` 77 | 78 | Note how each response returns one of the three live nodes at random, 79 | with a TTL of 4 seconds. 80 | 81 | ## dns-loadbalancer-rr.py 82 | The Python program `dns-loadbalancer-rr.py` is a second variant of DNS-based 83 | load balancing. Whereas `dns-loadbalancer.py` returns one random Scylla 84 | node in response to every request, `dns-loadbalancer-rr.py` returns the 85 | *entire* list of live nodes, shifted cyclically by a random amount. 86 | This technique, [Round-robin DNS](https://en.wikipedia.org/wiki/Round-robin_DNS) 87 | allows clients which can make use of the entire list to use it - while 88 | clients that can't use multiple response records, and just use the first 89 | one will still get a random node. However, the intention is that retrieving 90 | a list instead of one node will allow DNS caching while still not getting 91 | stuck on a single node for an entire client process or even machine. 92 | 93 | Again, this implementation is not meant to be used for production workloads, 94 | but more as a *proof of concept*, of what can be done. Again the file 95 | `dns-loadbalancer-rr.py` should be edited to change the Alternator port 96 | number (which must be identical across the cluster) - in the `alternator_port` 97 | variable - and also an initial list of known Scylla nodes in the `livenodes` 98 | variable. 99 | 100 | ### Example 101 | 102 | In the following example, Scylla is running on port 8000 on four 103 | IP addresses - 127.0.0.1, 127.0.0.2, 127.0.0.3 and 127.0.0.4. 104 | The initial `livenodes` list contains just 127.0.0.1. 105 | 106 | ``` 107 | $ sudo ./dns-loadbalancer-rr.py 108 | updating livenodes from http://127.0.0.1:8000/localnodes 109 | ['127.0.0.4', '127.0.0.1', '127.0.0.3', '127.0.0.2'] 110 | ... 111 | ``` 112 | 113 | ``` 114 | $ dig @localhost alternator.example.com 115 | ... 116 | ;; ANSWER SECTION: 117 | alternator.example.com. 5 IN A 127.0.0.1 118 | alternator.example.com. 5 IN A 127.0.0.3 119 | alternator.example.com. 5 IN A 127.0.0.2 120 | alternator.example.com. 5 IN A 127.0.0.4 121 | $ dig @localhost alternator.example.com 122 | ... 123 | ;; ANSWER SECTION: 124 | alternator.example.com. 5 IN A 127.0.0.3 125 | alternator.example.com. 5 IN A 127.0.0.2 126 | alternator.example.com. 5 IN A 127.0.0.4 127 | alternator.example.com. 5 IN A 127.0.0.1 128 | ``` 129 | 130 | Note how the second response is a cyclically shifted version of the first 131 | one, but both list all four nodes. 132 | -------------------------------------------------------------------------------- /dns/dns-loadbalancer-rr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # This proof-of-concept is a variant of dns-loadbalancer.py. Whereas the 4 | # latter returns one random Scylla node in response to every request, the 5 | # implementation in this file returns the *entire* list of live nodes, shifted 6 | # cyclically by a random amount. This technique allows clients which can make 7 | # use of the entire list to use it - while clients who can't and use the entire 8 | # list and use the first one will still get a random node, as happens in 9 | # dns-loadbalancer.py. This technique is known as "Round-robin DNS" - see 10 | # https://en.wikipedia.org/wiki/Round-robin_DNS 11 | 12 | import dnslib.server 13 | import dnslib 14 | import random 15 | import _thread 16 | import urllib.request 17 | import time 18 | 19 | # The list of live nodes, all of them supposedly answering HTTP requests on 20 | # alternator_port. All of these nodes will be returned, shifted by a random 21 | # amount, from every DNS request. 22 | # This list starts with one or more known nodes, but then the 23 | # livenodes_update() thread periodically replaces this list by an up-to-date 24 | # list retrieved from makeing a "/localnodes" requests to one of these nodes. 25 | livenodes = ['127.0.0.1'] 26 | alternator_port = 8000 27 | def livenodes_update(): 28 | global alternator_port 29 | global livenodes 30 | while True: 31 | # Contact one of the already known nodes by random, to fetch a new 32 | # list of known nodes. 33 | # TODO: We could reuse the HTTP connection (and prefer to make the 34 | # request to the same node again, and not a random node). 35 | ip = random.choice(livenodes) 36 | url = 'http://{}:{}/localnodes'.format(ip, alternator_port) 37 | print('updating livenodes from {}'.format(url)) 38 | try: 39 | nodes = urllib.request.urlopen(url, None, 1.0).read().decode('ascii') 40 | a = [x.strip('"').rstrip('"') for x in nodes.strip('[').rstrip(']').split(',')] 41 | # If we're successful, replace livenodes by the new list 42 | livenodes = a 43 | print(livenodes) 44 | except: 45 | # Contacting this ip was unsuccessful, we could remove remove it 46 | # from the list of live nodes, but tais is not a good idea if 47 | # all nodes are temporarily down. In any case, when we do reach 48 | # a live node, we'll replace the entire list. 49 | pass 50 | time.sleep(1) 51 | _thread.start_new_thread(livenodes_update,()) 52 | 53 | def random_shift(l): 54 | shift = random.randrange(len(l)) 55 | return l[shift::] + l[:shift:] 56 | 57 | class Resolver: 58 | def resolve(self, request, handler): 59 | qname = request.q.qname 60 | reply = request.reply() 61 | # Note responses have TTL 5, as in Amazon's Dynamo DNS 62 | for ip in random_shift(livenodes): 63 | reply.add_answer(*dnslib.RR.fromZone('{} 5 A {}'.format(qname, ip))) 64 | return reply 65 | 66 | resolver = Resolver() 67 | logger = dnslib.server.DNSLogger(prefix=True) 68 | tcp_server = dnslib.server.DNSServer(Resolver(), port=53, address='localhost', logger=logger, tcp=True) 69 | tcp_server.start_thread() 70 | udp_server = dnslib.server.DNSServer(Resolver(), port=53, address='localhost', logger=logger, tcp=False) 71 | udp_server.start_thread() 72 | 73 | try: 74 | while True: 75 | time.sleep(10) 76 | except KeyboardInterrupt: 77 | print('Goodbye!') 78 | finally: 79 | tcp_server.stop() 80 | udp_server.stop() 81 | -------------------------------------------------------------------------------- /dns/dns-loadbalancer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import dnslib.server 3 | import dnslib 4 | import random 5 | import _thread 6 | import urllib.request 7 | import time 8 | 9 | # The list of live nodes, all of them supposedly answering HTTP requests on 10 | # alternator_port. One of these nodes will be returned at random from every 11 | # DNS request. This list starts with one or more known nodes, but then the 12 | # livenodes_update() thread periodically replaces this list by an up-to-date 13 | # list retrieved from makeing a "localnodes" requests to one of these nodes. 14 | livenodes = ['127.0.0.1'] 15 | alternator_port = 8000 16 | def livenodes_update(): 17 | global alternator_port 18 | global livenodes 19 | while True: 20 | # Contact one of the already known nodes by random, to fetch a new 21 | # list of known nodes. 22 | ip = random.choice(livenodes) 23 | url = 'http://{}:{}/localnodes'.format(ip, alternator_port) 24 | print('updating livenodes from {}'.format(url)) 25 | try: 26 | nodes = urllib.request.urlopen(url, None, 1.0).read().decode('ascii') 27 | a = [x.strip('"').rstrip('"') for x in nodes.strip('[').rstrip(']').split(',')] 28 | # If we're successful, replace livenodes by the new list 29 | livenodes = a 30 | print(livenodes) 31 | except: 32 | # TODO: contacting this ip was unsuccessful, maybe we should 33 | # remove it from the list of live nodes. 34 | pass 35 | time.sleep(1) 36 | _thread.start_new_thread(livenodes_update,()) 37 | 38 | class Resolver: 39 | def resolve(self, request, handler): 40 | qname = request.q.qname 41 | reply = request.reply() 42 | # Note responses have TTL 4, as in Amazon's Dynamo DNS 43 | ip = random.choice(livenodes) 44 | reply.add_answer(*dnslib.RR.fromZone('{} 4 A {}'.format(qname, ip))) 45 | return reply 46 | resolver = Resolver() 47 | logger = dnslib.server.DNSLogger(prefix=True) 48 | tcp_server = dnslib.server.DNSServer(Resolver(), port=53, address='localhost', logger=logger, tcp=True) 49 | tcp_server.start_thread() 50 | udp_server = dnslib.server.DNSServer(Resolver(), port=53, address='localhost', logger=logger, tcp=False) 51 | udp_server.start_thread() 52 | 53 | try: 54 | while True: 55 | time.sleep(10) 56 | except KeyboardInterrupt: 57 | print('Goodbye!') 58 | finally: 59 | tcp_server.stop() 60 | udp_server.stop() 61 | -------------------------------------------------------------------------------- /go/.golangci.yml: -------------------------------------------------------------------------------- 1 | issues: 2 | new: true 3 | new-from-rev: origin/master 4 | exclude-dirs: 5 | - semver 6 | linters: 7 | disable-all: true 8 | enable: 9 | - errcheck 10 | - gocritic 11 | - gofumpt 12 | - goheader 13 | - goimports 14 | - gosimple 15 | - govet 16 | - ineffassign 17 | - misspell 18 | - predeclared 19 | - revive 20 | - staticcheck 21 | - thelper 22 | - tparallel 23 | - typecheck 24 | - unused 25 | - forbidigo 26 | run: 27 | allow-parallel-runners: true 28 | deadline: 10m 29 | modules-download-mode: readonly 30 | tests: true 31 | build-tags: [integration] 32 | go: '1.23.0' 33 | linters-settings: 34 | govet: 35 | enable-all: true 36 | disable: 37 | - shadow 38 | - fieldalignment 39 | gofumpt: 40 | extra-rules: true 41 | revive: 42 | rules: 43 | - name: var-naming 44 | disabled: true 45 | -------------------------------------------------------------------------------- /go/Makefile: -------------------------------------------------------------------------------- 1 | define dl_tgz 2 | @if ! $(1) 2>/dev/null 1>&2; then \ 3 | [ -d "$(GOBIN)" ] || mkdir "$(GOBIN)"; \ 4 | if [ ! -f "$(GOBIN)/$(1)" ]; then \ 5 | echo "Downloading $(GOBIN)/$(1)"; \ 6 | curl --progress-bar -L $(2) | tar zxf - --wildcards --strip 1 -C $(GOBIN) '*/$(1)'; \ 7 | chmod +x "$(GOBIN)/$(1)"; \ 8 | fi; \ 9 | fi 10 | endef 11 | 12 | define dl_bin 13 | @if ! $(1) 2>/dev/null 1>&2; then \ 14 | [ -d "$(GOBIN)" ] || mkdir "$(GOBIN)"; \ 15 | if [ ! -f "$(GOBIN)/$(1)" ]; then \ 16 | echo "Downloading $(GOBIN)/$(1)"; \ 17 | curl --progress-bar -L $(2) --output "$(GOBIN)/$(1)"; \ 18 | chmod +x "$(GOBIN)/$(1)"; \ 19 | fi; \ 20 | fi 21 | endef 22 | 23 | MAKEFILE_PATH := $(abspath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))) 24 | GOOS := $(shell uname | tr '[:upper:]' '[:lower:]') 25 | GOARCH := $(shell go env GOARCH) 26 | DOCKER_COMPOSE_VERSION := 2.34.0 27 | GOLANGCI_VERSION := 1.64.8 28 | 29 | GOLANGCI_DOWNLOAD_URL := "https://github.com/golangci/golangci-lint/releases/download/v$(GOLANGCI_VERSION)/golangci-lint-$(GOLANGCI_VERSION)-$(GOOS)-amd64.tar.gz" 30 | 31 | ifeq ($(GOARCH),arm64) 32 | DOCKER_COMPOSE_DOWNLOAD_URL := "https://github.com/docker/compose/releases/download/v$(DOCKER_COMPOSE_VERSION)/docker-compose-$(GOOS)-aarch64" 33 | else ifeq ($(GOARCH),amd64) 34 | DOCKER_COMPOSE_DOWNLOAD_URL := "https://github.com/docker/compose/releases/download/v$(DOCKER_COMPOSE_VERSION)/docker-compose-$(GOOS)-x86_64" 35 | else 36 | @printf 'Unknown architecture "%s"\n', "$(GOARCH)" 37 | @exit 69 38 | endif 39 | 40 | 41 | ifndef GOBIN 42 | export GOBIN := $(MAKEFILE_PATH)/bin 43 | endif 44 | 45 | export PATH := $(GOBIN):$(PATH) 46 | 47 | COMPOSE := docker-compose -f $(MAKEFILE_PATH)/test/docker-compose.yml 48 | 49 | .PHONY: clean 50 | clean: 51 | @echo "Cleaning v1" 52 | @cd v1 && go clean -r && cd .. 53 | @echo "Cleaning v2" 54 | @cd v2 && go clean -r && cd .. 55 | 56 | .PHONY: build 57 | build: 58 | @echo "Building v1" 59 | @cd v1 && go build ./... && cd .. 60 | @echo "Building v2" 61 | @cd v2 && go build ./... && cd .. 62 | 63 | .PHONY: clean-caches 64 | clean-caches: 65 | @go clean -r -cache -testcache -modcache ./... 66 | 67 | .PHONY: check 68 | check: check-golangci 69 | 70 | .PHONY: fix 71 | fix: fix-golangci 72 | 73 | .PHONY: check-golangci 74 | check-golangci: $(GOBIN)/golangci-lint 75 | @echo "======== Lint code for v1" 76 | @cd v1 && golangci-lint run --config=../.golangci.yml ./... 77 | @cd .. 78 | @echo "======== Lint code for v2" 79 | @cd v2 && golangci-lint run --config=../.golangci.yml ./... 80 | 81 | .PHONY: fix-golangci 82 | fix-golangci: $(GOBIN)/golangci-lint 83 | @echo "======== Fix code for v1" 84 | @cd v1 && golangci-lint run --fix --config=../.golangci.yml ./... 85 | @cd .. 86 | @echo "======== Fix code for v2" 87 | @cd v2 && golangci-lint run --fix --config=../.golangci.yml ./... 88 | 89 | .PHONY: test 90 | test: build check unit-test integration-test 91 | 92 | .PHONY: unit-test 93 | unit-test: 94 | @echo "======== Running unit tests for v1" 95 | @cd v1 && go test -v -cover -race ./... 96 | @cd .. 97 | @echo "======== Running unit tests for v2" 98 | @cd v2 && go test -v -cover -race ./... 99 | 100 | .PHONY: integration-test 101 | integration-test: scylla-up 102 | @echo "======== Running unit tests for v1" 103 | @cd v1 && go test -v -cover -race -tags integration ./... 104 | @cd .. 105 | @echo "======== Running unit tests for v2" 106 | @cd v2 && go test -v -cover -race -tags integration ./... 107 | 108 | .PHONY: .prepare-cert 109 | .prepare-cert: 110 | @[ -f "test/scylla/db.key" ] || (echo "Prepare certificate" && cd test/scylla/ && openssl req -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -x509 -newkey rsa:4096 -keyout db.key -out db.crt -days 3650 -nodes && chmod 644 db.key) 111 | 112 | .PHONY: scylla-up 113 | scylla-up: .prepare-cert $(GOBIN)/docker-compose 114 | @sudo sysctl -w fs.aio-max-nr=10485760 115 | $(COMPOSE) up -d 116 | 117 | .PHONY: scylla-down 118 | scylla-down: $(GOBIN)/docker-compose 119 | $(COMPOSE) down 120 | 121 | .PHONY: scylla-kill 122 | scylla-kill: $(GOBIN)/docker-compose 123 | $(COMPOSE) kill 124 | 125 | .PHONY: scylla-rm 126 | scylla-rm: $(GOBIN)/docker-compose 127 | $(COMPOSE) rm -f 128 | 129 | $(GOBIN)/golangci-lint: Makefile 130 | $(call dl_tgz,golangci-lint,$(GOLANGCI_DOWNLOAD_URL)) 131 | 132 | $(GOBIN)/docker-compose: Makefile 133 | $(call dl_bin,docker-compose,$(DOCKER_COMPOSE_DOWNLOAD_URL)) 134 | -------------------------------------------------------------------------------- /go/README.md: -------------------------------------------------------------------------------- 1 | # Alternator - Client-side load balancing - Go 2 | 3 | ## Introduction 4 | 5 | As explained in the [toplevel README](../README.md), DynamoDB applications 6 | are usually aware of a _single endpoint_, a single URL to which they 7 | connect - e.g., `http://dynamodb.us-east-1.amazonaws.com`. But Alternator 8 | is distributed over a cluster of nodes and we would like the application to 9 | send requests to all these nodes - not just to one. This is important for two 10 | reasons: **high availability** (the failure of a single Alternator node should 11 | not prevent the client from proceeding) and **load balancing** over all 12 | Alternator nodes. 13 | 14 | One of the ways to do this is to provide a modified library, which will 15 | allow a mostly-unmodified application which is only aware of one 16 | "enpoint URL" to send its requests to many different Alternator nodes. 17 | 18 | Our intention is _not_ to fork the existing AWS client library for Go. 19 | Rather, our intention is to provide a small library which tacks on to 20 | the existing "aws-sdk-go" library which the application is already using, 21 | and makes it do the right thing for Alternator. 22 | 23 | ## The `alternatorlb` library 24 | 25 | The Go integration exists in two flavors. The [v1](v1) directory contains 26 | a snippet for integrating Alternator with the `aws-sdk-go-v1`, while the 27 | [v2](v2) directory contains a Go module, that can be used with 28 | the `aws-sdk-go-v2`. See the README.md of these respective directories for more 29 | details on how to use them. 30 | -------------------------------------------------------------------------------- /go/common/cert_source.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "os" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type CertSource struct { 12 | certPath string 13 | keyPath string 14 | cert *tls.Certificate 15 | mutex sync.Mutex 16 | modTime time.Time 17 | } 18 | 19 | func NewFileCertificate(certPath, keyPath string) *CertSource { 20 | return &CertSource{ 21 | certPath: certPath, 22 | keyPath: keyPath, 23 | } 24 | } 25 | 26 | func NewCertificate(certificate tls.Certificate) *CertSource { 27 | return &CertSource{ 28 | cert: &certificate, 29 | } 30 | } 31 | 32 | func (c *CertSource) GetCertificate() (*tls.Certificate, error) { 33 | if c.certPath == "" { 34 | return c.cert, nil 35 | } 36 | 37 | c.mutex.Lock() 38 | defer c.mutex.Unlock() 39 | 40 | certStat, err := os.Stat(c.certPath) 41 | if err != nil { 42 | err = fmt.Errorf("failed to stat certificate file %s: %w", c.certPath, err) 43 | if c.cert != nil { 44 | LogError(err) 45 | return c.cert, nil 46 | } 47 | return nil, err 48 | } 49 | 50 | if c.cert != nil && certStat.ModTime().Equal(c.modTime) { 51 | return c.cert, nil // Return cached certificate if unchanged 52 | } 53 | 54 | cert, err := tls.LoadX509KeyPair(c.certPath, c.keyPath) 55 | if err != nil { 56 | err = fmt.Errorf("failed to load certificate file %s: %w", c.certPath, err) 57 | if c.cert != nil { 58 | LogError(err) 59 | return c.cert, nil 60 | } 61 | return nil, err 62 | } 63 | 64 | c.cert = &cert 65 | c.modTime = certStat.ModTime() 66 | return c.cert, nil 67 | } 68 | -------------------------------------------------------------------------------- /go/common/config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | type Config struct { 12 | Port int 13 | Scheme string 14 | Rack string 15 | Datacenter string 16 | AWSRegion string 17 | NodesListUpdatePeriod time.Duration 18 | AccessKeyID string 19 | SecretAccessKey string 20 | HTTPClient *http.Client 21 | ALNHTTPClient *http.Client 22 | ClientCertificateSource *CertSource 23 | // Makes it ignore server certificate errors 24 | IgnoreServerCertificateError bool 25 | // OptimizeHeaders - when true removes unnecessary http headers reducing network footprint 26 | OptimizeHeaders bool 27 | // Update node list when no requests are running 28 | IdleNodesListUpdatePeriod time.Duration 29 | // A key writer for pre master key: https://wiki.wireshark.org/TLS#using-the-pre-master-secret 30 | KeyLogWriter io.Writer 31 | // TLS session cache 32 | TLSSessionCache tls.ClientSessionCache 33 | // Maximum number of idle HTTP connections 34 | MaxIdleHTTPConnections int 35 | } 36 | 37 | type Option func(config *Config) 38 | 39 | const ( 40 | defaultPort = 8080 41 | defaultScheme = "http" 42 | defaultAWSRegion = "default-alb-region" 43 | ) 44 | 45 | var defaultTLSSessionCache = tls.NewLRUClientSessionCache(256) 46 | 47 | func NewConfig() *Config { 48 | return &Config{ 49 | Port: defaultPort, 50 | Scheme: defaultScheme, 51 | AWSRegion: defaultAWSRegion, 52 | NodesListUpdatePeriod: 5 * time.Minute, 53 | IdleNodesListUpdatePeriod: 2 * time.Hour, 54 | TLSSessionCache: defaultTLSSessionCache, 55 | MaxIdleHTTPConnections: 100, 56 | } 57 | } 58 | 59 | func (c *Config) ToALNConfig() ALNConfig { 60 | cfg := NewALNConfig() 61 | for _, opt := range c.ToALNOptions() { 62 | opt(&cfg) 63 | } 64 | return cfg 65 | } 66 | 67 | func (c *Config) ToALNOptions() []ALNOption { 68 | out := []ALNOption{ 69 | WithALNPort(c.Port), 70 | WithALNScheme(c.Scheme), 71 | WithALNUpdatePeriod(c.NodesListUpdatePeriod), 72 | WithALNIgnoreServerCertificateError(c.IgnoreServerCertificateError), 73 | WithALNMaxIdleHTTPConnections(c.MaxIdleHTTPConnections), 74 | } 75 | 76 | if c.Rack != "" { 77 | out = append(out, WithALNRack(c.Rack)) 78 | } 79 | 80 | if c.Datacenter != "" { 81 | out = append(out, WithALNDatacenter(c.Datacenter)) 82 | } 83 | 84 | if c.ALNHTTPClient != nil { 85 | out = append(out, WithALNHTTPClient(c.HTTPClient)) 86 | } 87 | 88 | if c.IdleNodesListUpdatePeriod != 0 { 89 | out = append(out, WithALNIdleUpdatePeriod(c.IdleNodesListUpdatePeriod)) 90 | } 91 | 92 | if c.ClientCertificateSource != nil { 93 | out = append(out, WithALNClientCertificateSource(c.ClientCertificateSource)) 94 | } 95 | 96 | if c.KeyLogWriter != nil { 97 | out = append(out, WithALNKeyLogWriter(c.KeyLogWriter)) 98 | } 99 | 100 | if c.TLSSessionCache != nil { 101 | out = append(out, WithALNTLSSessionCache(c.TLSSessionCache)) 102 | } 103 | return out 104 | } 105 | 106 | func WithScheme(scheme string) Option { 107 | return func(config *Config) { 108 | config.Scheme = scheme 109 | } 110 | } 111 | 112 | func WithPort(port int) Option { 113 | return func(config *Config) { 114 | config.Port = port 115 | } 116 | } 117 | 118 | func WithRack(rack string) Option { 119 | return func(config *Config) { 120 | config.Rack = rack 121 | } 122 | } 123 | 124 | func WithDatacenter(dc string) Option { 125 | return func(config *Config) { 126 | config.Datacenter = dc 127 | } 128 | } 129 | 130 | func WithAWSRegion(region string) Option { 131 | return func(config *Config) { 132 | config.AWSRegion = region 133 | } 134 | } 135 | 136 | func WithNodesListUpdatePeriod(period time.Duration) Option { 137 | return func(config *Config) { 138 | config.NodesListUpdatePeriod = period 139 | } 140 | } 141 | 142 | func WithCredentials(accessKeyID string, secretAccessKey string) Option { 143 | return func(config *Config) { 144 | config.AccessKeyID = accessKeyID 145 | config.SecretAccessKey = secretAccessKey 146 | } 147 | } 148 | 149 | func WithHTTPClient(httpClient *http.Client) Option { 150 | return func(config *Config) { 151 | config.HTTPClient = httpClient 152 | } 153 | } 154 | 155 | func WithLocalNodesReaderHTTPClient(httpClient *http.Client) Option { 156 | return func(config *Config) { 157 | config.ALNHTTPClient = httpClient 158 | } 159 | } 160 | 161 | func WithClientCertificateFile(certFile, keyFile string) Option { 162 | return func(config *Config) { 163 | config.ClientCertificateSource = NewFileCertificate(certFile, keyFile) 164 | } 165 | } 166 | 167 | func WithClientCertificate(certificate tls.Certificate) Option { 168 | return func(config *Config) { 169 | config.ClientCertificateSource = NewCertificate(certificate) 170 | } 171 | } 172 | 173 | func WithClientCertificateSource(source *CertSource) Option { 174 | return func(config *Config) { 175 | config.ClientCertificateSource = source 176 | } 177 | } 178 | 179 | func WithIgnoreServerCertificateError(value bool) Option { 180 | return func(config *Config) { 181 | config.IgnoreServerCertificateError = value 182 | } 183 | } 184 | 185 | func WithOptimizeHeaders() Option { 186 | return func(config *Config) { 187 | config.OptimizeHeaders = true 188 | } 189 | } 190 | 191 | func WithIdleNodesListUpdatePeriod(period time.Duration) Option { 192 | return func(config *Config) { 193 | config.IdleNodesListUpdatePeriod = period 194 | } 195 | } 196 | 197 | func WithKeyLogWriter(writer io.Writer) Option { 198 | return func(config *Config) { 199 | config.KeyLogWriter = writer 200 | } 201 | } 202 | 203 | func WithTLSSessionCache(cache tls.ClientSessionCache) Option { 204 | return func(config *Config) { 205 | config.TLSSessionCache = cache 206 | } 207 | } 208 | 209 | func WithMaxIdleHTTPConnections(value int) Option { 210 | return func(config *Config) { 211 | config.MaxIdleHTTPConnections = value 212 | } 213 | } 214 | 215 | func PatchHTTPClient(config Config, client interface{}) error { 216 | httpClient, ok := client.(*http.Client) 217 | if !ok { 218 | return errors.New("config is not a http client") 219 | } 220 | alnConfig := config.ToALNConfig() 221 | 222 | if httpClient.Transport == nil { 223 | httpClient.Transport = DefaultHTTPTransport() 224 | } 225 | 226 | httpTransport, ok := httpClient.Transport.(*http.Transport) 227 | if !ok { 228 | return errors.New("failed to patch http transport for ignore server certificate") 229 | } 230 | PatchBasicHTTPTransport(alnConfig, httpTransport) 231 | 232 | if config.OptimizeHeaders { 233 | allowedHeaders := []string{"Host", "X-Amz-Target", "Content-Length", "Accept-Encoding"} 234 | if config.AccessKeyID != "" { 235 | allowedHeaders = append(allowedHeaders, "Authorization", "X-Amz-Date") 236 | } 237 | httpClient.Transport = NewHeaderWhiteListingTransport(httpTransport, allowedHeaders...) 238 | } 239 | return nil 240 | } 241 | -------------------------------------------------------------------------------- /go/common/go.mod: -------------------------------------------------------------------------------- 1 | module common 2 | 3 | go 1.22.6 4 | -------------------------------------------------------------------------------- /go/common/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 4 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 8 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 9 | -------------------------------------------------------------------------------- /go/common/header_whitelist.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net/http" 5 | "slices" 6 | "strings" 7 | ) 8 | 9 | type HeaderWhiteListing struct { 10 | allowedHeaders []string 11 | original http.RoundTripper 12 | } 13 | 14 | func NewHeaderWhiteListingTransport(original http.RoundTripper, allowedHeaders ...string) *HeaderWhiteListing { 15 | for id, h := range allowedHeaders { 16 | allowedHeaders[id] = strings.ToLower(h) 17 | } 18 | return &HeaderWhiteListing{ 19 | allowedHeaders: allowedHeaders, 20 | original: original, 21 | } 22 | } 23 | 24 | func (h HeaderWhiteListing) RoundTrip(r *http.Request) (*http.Response, error) { 25 | newHeaders := http.Header{} 26 | for headerName := range r.Header { 27 | if slices.Contains(h.allowedHeaders, strings.ToLower(headerName)) { 28 | newHeaders.Set(headerName, r.Header.Get(headerName)) 29 | } 30 | } 31 | r.Header = newHeaders 32 | return h.original.RoundTrip(r) 33 | } 34 | 35 | var _ http.RoundTripper = HeaderWhiteListing{} 36 | -------------------------------------------------------------------------------- /go/common/live_nodes.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | "sync/atomic" 14 | "time" 15 | ) 16 | 17 | const ( 18 | defaultUpdatePeriod = time.Second * 10 19 | defaultIdleConnectionTimeout = 6 * time.Hour 20 | ) 21 | 22 | type AlternatorLiveNodes struct { 23 | liveNodes atomic.Pointer[[]url.URL] 24 | initialNodes []url.URL 25 | nextLiveNodeIdx atomic.Uint64 26 | cfg ALNConfig 27 | nextUpdate atomic.Int64 28 | idleUpdaterStarted atomic.Bool 29 | ctx context.Context 30 | stopFn context.CancelFunc 31 | httpClient *http.Client 32 | updateSignal chan struct{} 33 | } 34 | 35 | type ALNConfig struct { 36 | Scheme string 37 | Port int 38 | Rack string 39 | Datacenter string 40 | UpdatePeriod time.Duration 41 | // Now often read /localnodes when no requests are going through 42 | IdleUpdatePeriod time.Duration 43 | HTTPClient *http.Client 44 | // Makes it ignore server certificate errors 45 | IgnoreServerCertificateError bool 46 | ClientCertificateSource *CertSource 47 | // A key writer for pre master key: https://wiki.wireshark.org/TLS#using-the-pre-master-secret 48 | KeyLogWriter io.Writer 49 | // TLS session cache 50 | TLSSessionCache tls.ClientSessionCache 51 | MaxIdleHTTPConnections int 52 | } 53 | 54 | func NewALNConfig() ALNConfig { 55 | return ALNConfig{ 56 | Scheme: defaultScheme, 57 | Port: defaultPort, 58 | Rack: "", 59 | Datacenter: "", 60 | UpdatePeriod: defaultUpdatePeriod, 61 | IdleUpdatePeriod: 0, // Don't update by default 62 | HTTPClient: nil, 63 | TLSSessionCache: defaultTLSSessionCache, 64 | MaxIdleHTTPConnections: 100, 65 | } 66 | } 67 | 68 | type ALNOption func(config *ALNConfig) 69 | 70 | func WithALNScheme(scheme string) ALNOption { 71 | return func(config *ALNConfig) { 72 | config.Scheme = scheme 73 | } 74 | } 75 | 76 | func WithALNPort(port int) ALNOption { 77 | return func(config *ALNConfig) { 78 | config.Port = port 79 | } 80 | } 81 | 82 | func WithALNRack(rack string) ALNOption { 83 | return func(config *ALNConfig) { 84 | config.Rack = rack 85 | } 86 | } 87 | 88 | func WithALNDatacenter(datacenter string) ALNOption { 89 | return func(config *ALNConfig) { 90 | config.Datacenter = datacenter 91 | } 92 | } 93 | 94 | func WithALNUpdatePeriod(period time.Duration) ALNOption { 95 | return func(config *ALNConfig) { 96 | config.UpdatePeriod = period 97 | } 98 | } 99 | 100 | func WithALNIdleUpdatePeriod(period time.Duration) ALNOption { 101 | return func(config *ALNConfig) { 102 | config.IdleUpdatePeriod = period 103 | } 104 | } 105 | 106 | func WithALNHTTPClient(client *http.Client) ALNOption { 107 | return func(config *ALNConfig) { 108 | config.HTTPClient = client 109 | } 110 | } 111 | 112 | func WithALNIgnoreServerCertificateError(value bool) ALNOption { 113 | return func(config *ALNConfig) { 114 | config.IgnoreServerCertificateError = value 115 | } 116 | } 117 | 118 | func WithALNClientCertificateFile(certFile, keyFile string) ALNOption { 119 | return func(config *ALNConfig) { 120 | config.ClientCertificateSource = NewFileCertificate(certFile, keyFile) 121 | } 122 | } 123 | 124 | func WithALNClientCertificate(certificate tls.Certificate) ALNOption { 125 | return func(config *ALNConfig) { 126 | config.ClientCertificateSource = NewCertificate(certificate) 127 | } 128 | } 129 | 130 | func WithALNClientCertificateSource(source *CertSource) ALNOption { 131 | return func(config *ALNConfig) { 132 | config.ClientCertificateSource = source 133 | } 134 | } 135 | 136 | func WithALNKeyLogWriter(writer io.Writer) ALNOption { 137 | return func(config *ALNConfig) { 138 | config.KeyLogWriter = writer 139 | } 140 | } 141 | 142 | func WithALNTLSSessionCache(cache tls.ClientSessionCache) ALNOption { 143 | return func(config *ALNConfig) { 144 | config.TLSSessionCache = cache 145 | } 146 | } 147 | 148 | func WithALNMaxIdleHTTPConnections(value int) ALNOption { 149 | return func(config *ALNConfig) { 150 | config.MaxIdleHTTPConnections = value 151 | } 152 | } 153 | 154 | func NewAlternatorLiveNodes(initialNodes []string, options ...ALNOption) (*AlternatorLiveNodes, error) { 155 | if len(initialNodes) == 0 { 156 | return nil, errors.New("liveNodes cannot be empty") 157 | } 158 | 159 | cfg := NewALNConfig() 160 | for _, opt := range options { 161 | opt(&cfg) 162 | } 163 | 164 | httpClient := cfg.HTTPClient 165 | if httpClient == nil { 166 | httpClient = &http.Client{ 167 | Transport: NewHTTPTransport(cfg), 168 | } 169 | } 170 | 171 | nodes := make([]url.URL, len(initialNodes)) 172 | for i, node := range initialNodes { 173 | parsed, err := url.Parse(fmt.Sprintf("%s://%s:%d", cfg.Scheme, node, cfg.Port)) 174 | if err != nil { 175 | return nil, fmt.Errorf("invalid node URI: %v", err) 176 | } 177 | nodes[i] = *parsed 178 | } 179 | 180 | ctx, cancel := context.WithCancel(context.Background()) 181 | out := &AlternatorLiveNodes{ 182 | initialNodes: nodes, 183 | cfg: cfg, 184 | ctx: ctx, 185 | stopFn: cancel, 186 | httpClient: httpClient, 187 | updateSignal: make(chan struct{}, 1), 188 | } 189 | 190 | out.liveNodes.Store(&nodes) 191 | return out, nil 192 | } 193 | 194 | func (aln *AlternatorLiveNodes) triggerUpdate() { 195 | if aln.cfg.UpdatePeriod <= 0 { 196 | return 197 | } 198 | nextUpdate := aln.nextUpdate.Load() 199 | current := time.Now().UTC().Unix() 200 | if nextUpdate < current { 201 | if aln.nextUpdate.CompareAndSwap(nextUpdate, current+int64(aln.cfg.UpdatePeriod.Seconds())) { 202 | select { 203 | case aln.updateSignal <- struct{}{}: 204 | default: 205 | } 206 | } 207 | } 208 | } 209 | 210 | func (aln *AlternatorLiveNodes) startIdleUpdater() { 211 | if aln.cfg.IdleUpdatePeriod <= 0 { 212 | return 213 | } 214 | if aln.idleUpdaterStarted.CompareAndSwap(false, true) { 215 | go func() { 216 | t := time.NewTicker(aln.cfg.IdleUpdatePeriod) 217 | defer t.Stop() 218 | for { 219 | select { 220 | case <-aln.ctx.Done(): 221 | return 222 | case <-t.C: 223 | aln.nextUpdate.Store(time.Now().UTC().Unix() + int64(aln.cfg.UpdatePeriod.Seconds())) 224 | _ = aln.UpdateLiveNodes() 225 | case <-aln.updateSignal: 226 | aln.nextUpdate.Store(time.Now().UTC().Unix() + int64(aln.cfg.UpdatePeriod.Seconds())) 227 | _ = aln.UpdateLiveNodes() 228 | } 229 | } 230 | }() 231 | } 232 | } 233 | 234 | func (aln *AlternatorLiveNodes) Start() { 235 | aln.startIdleUpdater() 236 | } 237 | 238 | func (aln *AlternatorLiveNodes) Stop() { 239 | if aln.stopFn != nil { 240 | aln.stopFn() 241 | } 242 | } 243 | 244 | // NextNode gets next node, check if node list needs to be updated and run updating routine if needed 245 | func (aln *AlternatorLiveNodes) NextNode() url.URL { 246 | aln.startIdleUpdater() 247 | aln.triggerUpdate() 248 | return aln.nextNode() 249 | } 250 | 251 | func (aln *AlternatorLiveNodes) nextNode() url.URL { 252 | nodes := *aln.liveNodes.Load() 253 | if len(nodes) == 0 { 254 | nodes = aln.initialNodes 255 | } 256 | return nodes[aln.nextLiveNodeIdx.Add(1)%uint64(len(nodes))] 257 | } 258 | 259 | func (aln *AlternatorLiveNodes) nextAsURLWithPath(path, query string) *url.URL { 260 | base := aln.nextNode() 261 | newURL := base 262 | newURL.Path = path 263 | if query != "" { 264 | newURL.RawQuery = query 265 | } 266 | return &newURL 267 | } 268 | 269 | func (aln *AlternatorLiveNodes) UpdateLiveNodes() error { 270 | newNodes, err := aln.getNodes(aln.nextAsLocalNodesURL()) 271 | if err == nil && len(newNodes) > 0 { 272 | aln.liveNodes.Store(&newNodes) 273 | } 274 | return err 275 | } 276 | 277 | func (aln *AlternatorLiveNodes) getNodes(endpoint *url.URL) ([]url.URL, error) { 278 | resp, err := aln.httpClient.Get(endpoint.String()) 279 | if err != nil { 280 | return nil, err 281 | } 282 | defer resp.Body.Close() 283 | if resp.StatusCode != http.StatusOK { 284 | return nil, errors.New("non-200 response") 285 | } 286 | body, err := ioutil.ReadAll(resp.Body) 287 | if err != nil { 288 | return nil, err 289 | } 290 | 291 | var nodes []string 292 | if err := json.Unmarshal(body, &nodes); err != nil { 293 | return nil, err 294 | } 295 | 296 | var uris []url.URL 297 | for _, node := range nodes { 298 | nodeURL, err := url.Parse(fmt.Sprintf("%s://%s:%d", aln.cfg.Scheme, node, aln.cfg.Port)) 299 | if err != nil { 300 | continue 301 | } 302 | uris = append(uris, *nodeURL) 303 | } 304 | return uris, nil 305 | } 306 | 307 | func (aln *AlternatorLiveNodes) nextAsLocalNodesURL() *url.URL { 308 | query := "" 309 | if aln.cfg.Rack != "" { 310 | query += "rack=" + aln.cfg.Rack 311 | } 312 | if aln.cfg.Datacenter != "" { 313 | if query != "" { 314 | query += "&" 315 | } 316 | query += "dc=" + aln.cfg.Datacenter 317 | } 318 | return aln.nextAsURLWithPath("/localnodes", query) 319 | } 320 | 321 | func (aln *AlternatorLiveNodes) CheckIfRackAndDatacenterSetCorrectly() error { 322 | if aln.cfg.Rack == "" && aln.cfg.Datacenter == "" { 323 | return nil 324 | } 325 | newNodes, err := aln.getNodes(aln.nextAsLocalNodesURL()) 326 | if err != nil { 327 | return fmt.Errorf("failed to read list of nodes: %v", err) 328 | } 329 | if len(newNodes) == 0 { 330 | return errors.New("node returned empty list, datacenter or rack might be incorrect") 331 | } 332 | return nil 333 | } 334 | 335 | func (aln *AlternatorLiveNodes) CheckIfRackDatacenterFeatureIsSupported() (bool, error) { 336 | baseURI := aln.nextAsURLWithPath("/localnodes", "") 337 | fakeRackURI := aln.nextAsURLWithPath("/localnodes", "rack=fakeRack") 338 | 339 | hostsWithFakeRack, err := aln.getNodes(fakeRackURI) 340 | if err != nil { 341 | return false, err 342 | } 343 | hostsWithoutRack, err := aln.getNodes(baseURI) 344 | if err != nil { 345 | return false, err 346 | } 347 | if len(hostsWithoutRack) == 0 { 348 | return false, errors.New("host returned empty list") 349 | } 350 | 351 | return len(hostsWithFakeRack) != len(hostsWithoutRack), nil 352 | } 353 | -------------------------------------------------------------------------------- /go/common/utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | func DefaultHTTPTransport() *http.Transport { 12 | transport := http.DefaultTransport.(*http.Transport).Clone() 13 | transport.IdleConnTimeout = defaultIdleConnectionTimeout 14 | return transport 15 | } 16 | 17 | func NewHTTPTransport(config ALNConfig) *http.Transport { 18 | transport := DefaultHTTPTransport() 19 | PatchBasicHTTPTransport(config, transport) 20 | return transport 21 | } 22 | 23 | func PatchBasicHTTPTransport(config ALNConfig, transport *http.Transport) { 24 | transport.IdleConnTimeout = defaultIdleConnectionTimeout 25 | transport.MaxIdleConns = config.MaxIdleHTTPConnections 26 | 27 | if transport.TLSClientConfig == nil { 28 | transport.TLSClientConfig = &tls.Config{} 29 | } 30 | 31 | if config.KeyLogWriter != nil { 32 | transport.TLSClientConfig.KeyLogWriter = config.KeyLogWriter 33 | } 34 | 35 | if config.IgnoreServerCertificateError { 36 | transport.TLSClientConfig.InsecureSkipVerify = true 37 | transport.TLSClientConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { 38 | return nil 39 | } 40 | } 41 | 42 | if config.TLSSessionCache != nil { 43 | transport.TLSClientConfig.ClientSessionCache = config.TLSSessionCache 44 | } 45 | 46 | if config.ClientCertificateSource != nil { 47 | transport.TLSClientConfig.GetClientCertificate = func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) { 48 | return config.ClientCertificateSource.GetCertificate() 49 | } 50 | } 51 | } 52 | 53 | func LogError(err error) { 54 | _, _ = fmt.Fprintf(os.Stderr, "ERROR: %s", err.Error()) 55 | } 56 | -------------------------------------------------------------------------------- /go/test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | networks: 2 | public: 3 | name: alb_golang_network 4 | driver: bridge 5 | ipam: 6 | driver: default 7 | config: 8 | - subnet: 172.41.0.0/16 9 | 10 | services: 11 | scylla1: 12 | image: scylladb/scylla:2025.1 13 | networks: 14 | public: 15 | ipv4_address: 172.41.0.2 16 | command: | 17 | --rpc-address 172.41.0.2 18 | --listen-address 172.41.0.2 19 | --seeds 172.41.0.2 20 | --skip-wait-for-gossip-to-settle 0 21 | --ring-delay-ms 0 22 | --smp 2 23 | --memory 1G 24 | healthcheck: 25 | test: [ "CMD", "cqlsh", "scylla1", "-e", "select * from system.local WHERE key='local'" ] 26 | interval: 5s 27 | timeout: 5s 28 | retries: 10 29 | ports: 30 | - "19042" 31 | - "9999" 32 | - "9998" 33 | volumes: 34 | - ./scylla:/etc/scylla 35 | scylla2: 36 | image: scylladb/scylla:2025.1 37 | networks: 38 | public: 39 | ipv4_address: 172.41.0.3 40 | command: | 41 | --rpc-address 172.41.0.3 42 | --listen-address 172.41.0.3 43 | --seeds 172.41.0.2 44 | --skip-wait-for-gossip-to-settle 0 45 | --ring-delay-ms 0 46 | --smp 2 47 | --memory 1G 48 | healthcheck: 49 | test: [ "CMD", "cqlsh", "scylla2", "-e", "select * from system.local WHERE key='local'" ] 50 | interval: 5s 51 | timeout: 5s 52 | retries: 10 53 | ports: 54 | - "19042" 55 | - "9999" 56 | - "9998" 57 | depends_on: 58 | scylla1: 59 | condition: service_healthy 60 | volumes: 61 | - ./scylla:/etc/scylla 62 | scylla3: 63 | image: scylladb/scylla:2025.1 64 | networks: 65 | public: 66 | ipv4_address: 172.41.0.4 67 | command: | 68 | --rpc-address 172.41.0.4 69 | --listen-address 172.41.0.4 70 | --seeds 172.41.0.2,172.41.0.3 71 | --skip-wait-for-gossip-to-settle 0 72 | --ring-delay-ms 0 73 | --smp 2 74 | --memory 1G 75 | healthcheck: 76 | test: [ "CMD", "cqlsh", "scylla3", "-e", "select * from system.local WHERE key='local'" ] 77 | interval: 5s 78 | timeout: 5s 79 | retries: 10 80 | ports: 81 | - "19042" 82 | - "9999" 83 | - "9998" 84 | depends_on: 85 | scylla2: 86 | condition: service_healthy 87 | volumes: 88 | - ./scylla:/etc/scylla -------------------------------------------------------------------------------- /go/v1/README.md: -------------------------------------------------------------------------------- 1 | # Alternator - Client-side load balancing - Go 2 | 3 | ## Introduction 4 | 5 | As explained in the [toplevel README](../../README.md), DynamoDB applications 6 | are usually aware of a _single endpoint_, a single URL to which they 7 | connect - e.g., `http://dynamodb.us-east-1.amazonaws.com`. But Alternator 8 | is distributed over a cluster of nodes, and we would like the application to 9 | send requests to all these nodes - not just to one. This is important for two 10 | reasons: **high availability** (the failure of a single Alternator node should 11 | not prevent the client from proceeding) and **load balancing** over all 12 | Alternator nodes. 13 | 14 | One of the ways to do this is to provide a modified library, which will 15 | allow a mostly-unmodified application which is only aware of one 16 | "endpoint URL" to send its requests to many different Alternator nodes. 17 | 18 | Our intention is _not_ to fork the existing AWS client library for Go. 19 | Rather, our intention is to provide a small library which tacks on to 20 | the existing "aws-sdk-go" library which the application is already using, 21 | and makes it do the right thing for Alternator. 22 | 23 | ## The library 24 | 25 | The `AlternatorLB` class defined in `alternator_lb.go` can be used to 26 | easily change any application using `aws-sdk-go` from using Amazon DynamoDB 27 | to use Alternator: While DynamoDB only has one "endpoint", this class helps 28 | us balance the requests between all the nodes in the Alternator cluster. 29 | 30 | ## Using the library 31 | 32 | You create a regular `dynamodb.DynamoDB` client by one of the methods listed below and 33 | the rest of the application can use this dynamodb client normally 34 | this `db` object is thread-safe and can be used from multiple threads. 35 | 36 | This client will send requests to an Alternator nodes, instead of AWS DynamoDB. 37 | 38 | Every request performed on patched session will pick a different live 39 | Alternator node to send it to. Despite us sending different requests 40 | to different nodes, Go will keep these connections cached and reuse them 41 | when we send another request to the same node. 42 | 43 | ### Rack and Datacenter awareness 44 | 45 | You can configure load balancer to target particular datacenter (region) or rack (availability zone) via `WithRack` and `WithDatacenter` options, like so: 46 | ```golang 47 | lb, err := alb.NewAlternatorLB([]string{"x.x.x.x"}, alb.WithRack("someRack"), alb.WithDatacenter("someDc1")) 48 | ``` 49 | 50 | Additionally, you can check if alternator cluster know targeted rack/datacenter: 51 | ```golang 52 | if err := lb.CheckIfRackAndDatacenterSetCorrectly(); err != nil { 53 | return fmt.Errorf("CheckIfRackAndDatacenterSetCorrectly() unexpectedly returned an error: %v", err) 54 | } 55 | ``` 56 | 57 | To check if cluster support datacenter/rack feature supported you can call `CheckIfRackDatacenterFeatureIsSupported`: 58 | ```golang 59 | supported, err := lb.CheckIfRackDatacenterFeatureIsSupported() 60 | if err != nil { 61 | return fmt.Errorf("failed to check if rack/dc feature is supported: %v", err) 62 | } 63 | if !supported { 64 | return fmt.Errorf("dc/rack feature is not supporte") 65 | } 66 | ``` 67 | 68 | ### Spawn `dynamodb.DynamoDB` 69 | 70 | ```golang 71 | import ( 72 | "fmt" 73 | alb "alternator_loadbalancing" 74 | 75 | "github.com/aws/aws-sdk-go/aws" 76 | "github.com/aws/aws-sdk-go/service/dynamodb" 77 | ) 78 | 79 | func main() { 80 | lb, err := alb.NewAlternatorLB([]string{"x.x.x.x"}, alb.WithPort(9999)) 81 | if err != nil { 82 | panic(fmt.Sprintf("Error creating alternator load balancer: %v", err)) 83 | } 84 | ddb, err := lb.WithCredentials("whatever", "secret").NewDynamoDB() 85 | if err != nil { 86 | panic(fmt.Sprintf("Error creating dynamodb client: %v", err)) 87 | } 88 | _, _ = ddb.DeleteTable(...) 89 | } 90 | ``` 91 | 92 | ## Decrypting TLS 93 | 94 | Read wireshark wiki regarding decrypting TLS traffic: https://wiki.wireshark.org/TLS#using-the-pre-master-secret 95 | In order to obtain pre master key secrets, you need to provide a file writer into `alb.WithKeyLogWriter`, example: 96 | 97 | ```go 98 | keyWriter, err := os.OpenFile("/tmp/pre-master-key.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 99 | if err != nil { 100 | panic("Error opening key writer: " + err.Error()) 101 | } 102 | defer keyWriter.Close() 103 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithScheme("https"), alb.WithPort(httpsPort), alb.WithIgnoreServerCertificateError(true), alb.WithKeyLogWriter(keyWriter)) 104 | ``` 105 | 106 | Then you need to configure your traffic analyzer to read pre master key secrets from this file. 107 | 108 | ## Example 109 | 110 | You can find examples in `[alternator_lb_test.go](alternator_lb_test.go)` -------------------------------------------------------------------------------- /go/v1/alternator_lb.go: -------------------------------------------------------------------------------- 1 | package alternator_loadbalancing 2 | 3 | import ( 4 | "common" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/credentials" 11 | "github.com/aws/aws-sdk-go/aws/session" 12 | "github.com/aws/aws-sdk-go/service/dynamodb" 13 | ) 14 | 15 | type Option = common.Option 16 | 17 | var ( 18 | WithScheme = common.WithScheme 19 | WithPort = common.WithPort 20 | WithRack = common.WithRack 21 | WithDatacenter = common.WithDatacenter 22 | WithAWSRegion = common.WithAWSRegion 23 | WithNodesListUpdatePeriod = common.WithNodesListUpdatePeriod 24 | WithIdleNodesListUpdatePeriod = common.WithIdleNodesListUpdatePeriod 25 | WithCredentials = common.WithCredentials 26 | WithHTTPClient = common.WithHTTPClient 27 | WithLocalNodesReaderHTTPClient = common.WithLocalNodesReaderHTTPClient 28 | WithClientCertificateFile = common.WithClientCertificateFile 29 | WithClientCertificate = common.WithClientCertificate 30 | WithClientCertificateSource = common.WithClientCertificateSource 31 | WithIgnoreServerCertificateError = common.WithIgnoreServerCertificateError 32 | WithOptimizeHeaders = common.WithOptimizeHeaders 33 | WithKeyLogWriter = common.WithKeyLogWriter 34 | WithTLSSessionCache = common.WithTLSSessionCache 35 | WithMaxIdleHTTPConnections = common.WithMaxIdleHTTPConnections 36 | ) 37 | 38 | type AlternatorLB struct { 39 | nodes *common.AlternatorLiveNodes 40 | cfg common.Config 41 | } 42 | 43 | func NewAlternatorLB(initialNodes []string, options ...Option) (*AlternatorLB, error) { 44 | cfg := common.NewConfig() 45 | for _, opt := range options { 46 | opt(cfg) 47 | } 48 | 49 | nodes, err := common.NewAlternatorLiveNodes(initialNodes, cfg.ToALNOptions()...) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return &AlternatorLB{ 55 | nodes: nodes, 56 | cfg: *cfg, 57 | }, nil 58 | } 59 | 60 | func (lb *AlternatorLB) NextNode() url.URL { 61 | return lb.nodes.NextNode() 62 | } 63 | 64 | func (lb *AlternatorLB) UpdateLiveNodes() error { 65 | return lb.nodes.UpdateLiveNodes() 66 | } 67 | 68 | func (lb *AlternatorLB) CheckIfRackAndDatacenterSetCorrectly() error { 69 | return lb.nodes.CheckIfRackAndDatacenterSetCorrectly() 70 | } 71 | 72 | func (lb *AlternatorLB) CheckIfRackDatacenterFeatureIsSupported() (bool, error) { 73 | return lb.nodes.CheckIfRackDatacenterFeatureIsSupported() 74 | } 75 | 76 | func (lb *AlternatorLB) Start() { 77 | lb.nodes.Start() 78 | } 79 | 80 | func (lb *AlternatorLB) Stop() { 81 | lb.nodes.Stop() 82 | } 83 | 84 | // AWSConfig produces a conf for the AWS SDK that will integrate the alternator loadbalancing with the AWS SDK. 85 | func (lb *AlternatorLB) AWSConfig() (aws.Config, error) { 86 | cfg := aws.Config{ 87 | Endpoint: aws.String(fmt.Sprintf("%s://%s:%d", lb.cfg.Scheme, "dynamodb.fake.alterntor.cluster.node", lb.cfg.Port)), 88 | // Region is used in the signature algorithm so prevent request sent 89 | // to one region to be forward by an attacker to a different region. 90 | // But Alternator doesn't check it. It can be anything. 91 | Region: aws.String(lb.cfg.AWSRegion), 92 | } 93 | 94 | if lb.cfg.HTTPClient != nil { 95 | cfg.HTTPClient = lb.cfg.HTTPClient 96 | } else { 97 | cfg.HTTPClient = &http.Client{ 98 | Transport: common.DefaultHTTPTransport(), 99 | } 100 | } 101 | 102 | err := common.PatchHTTPClient(lb.cfg, cfg.HTTPClient) 103 | if err != nil { 104 | return cfg, err 105 | } 106 | 107 | if lb.cfg.AccessKeyID != "" && lb.cfg.SecretAccessKey != "" { 108 | // The third credential below, the session token, is only used for 109 | // temporary credentials, and is not supported by Alternator anyway. 110 | cfg.Credentials = credentials.NewStaticCredentials(lb.cfg.AccessKeyID, lb.cfg.SecretAccessKey, "") 111 | } 112 | 113 | cfg.HTTPClient.Transport = lb.wrapHTTPTransport(cfg.HTTPClient.Transport) 114 | return cfg, nil 115 | } 116 | 117 | func (lb *AlternatorLB) NewAWSSession() (*session.Session, error) { 118 | cfg, err := lb.AWSConfig() 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | return session.NewSessionWithOptions(session.Options{ 124 | Config: cfg, 125 | }) 126 | } 127 | 128 | // WithCredentials creates clone of AlternatorLB with altered alternator credentials 129 | func (lb *AlternatorLB) WithCredentials(accessKeyID, secretAccessKey string) *AlternatorLB { 130 | cfg := lb.cfg 131 | common.WithCredentials(accessKeyID, secretAccessKey)(&cfg) 132 | return &AlternatorLB{ 133 | nodes: lb.nodes, 134 | cfg: cfg, 135 | } 136 | } 137 | 138 | // WithAWSRegion creates clone of AlternatorLB with altered AWS region 139 | func (lb *AlternatorLB) WithAWSRegion(region string) *AlternatorLB { 140 | cfg := lb.cfg 141 | common.WithAWSRegion(region)(&cfg) 142 | return &AlternatorLB{ 143 | nodes: lb.nodes, 144 | cfg: cfg, 145 | } 146 | } 147 | 148 | func (lb *AlternatorLB) NewDynamoDB() (*dynamodb.DynamoDB, error) { 149 | sess, err := lb.NewAWSSession() 150 | if err != nil { 151 | return nil, err 152 | } 153 | return dynamodb.New(sess), nil 154 | } 155 | 156 | type roundTripper struct { 157 | originalTransport http.RoundTripper 158 | lb *AlternatorLB 159 | } 160 | 161 | func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 162 | node := rt.lb.NextNode() 163 | req.URL = &node 164 | return rt.originalTransport.RoundTrip(req) 165 | } 166 | 167 | func (lb *AlternatorLB) wrapHTTPTransport(original http.RoundTripper) http.RoundTripper { 168 | return &roundTripper{ 169 | originalTransport: original, 170 | lb: lb, 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /go/v1/alternator_lb_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package alternator_loadbalancing_test 5 | 6 | import ( 7 | "crypto/tls" 8 | "errors" 9 | "slices" 10 | "sync" 11 | "sync/atomic" 12 | "testing" 13 | 14 | "github.com/aws/aws-sdk-go/aws" 15 | "github.com/aws/aws-sdk-go/service/dynamodb" 16 | 17 | alb "alternator_loadbalancing" 18 | ) 19 | 20 | var ( 21 | knownNodes = []string{"172.41.0.2"} 22 | httpsPort = 9999 23 | httpPort = 9998 24 | ) 25 | 26 | var notFoundErr = new(*dynamodb.ResourceNotFoundException) 27 | 28 | func TestCheckIfRackAndDatacenterSetCorrectly_WrongDC(t *testing.T) { 29 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithPort(httpPort), alb.WithDatacenter("wrongDC")) 30 | if err != nil { 31 | t.Fatalf("Error creating alternator load balancer: %v", err) 32 | } 33 | defer lb.Stop() 34 | 35 | if lb.CheckIfRackAndDatacenterSetCorrectly() == nil { 36 | t.Fatalf("CheckIfRackAndDatacenterSetCorrectly() should have returned an error") 37 | } 38 | } 39 | 40 | func TestCheckIfRackAndDatacenterSetCorrectly_CorrectDC(t *testing.T) { 41 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithPort(httpPort), alb.WithDatacenter("datacenter1")) 42 | if err != nil { 43 | t.Fatalf("Error creating alternator load balancer: %v", err) 44 | } 45 | defer lb.Stop() 46 | 47 | if err := lb.CheckIfRackAndDatacenterSetCorrectly(); err != nil { 48 | t.Fatalf("CheckIfRackAndDatacenterSetCorrectly() unexpectedly returned an error: %v", err) 49 | } 50 | } 51 | 52 | func TestCheckIfRackAndDatacenterSetCorrectly_WrongRack(t *testing.T) { 53 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithPort(httpPort), alb.WithDatacenter("datacenter1"), alb.WithRack("wrongRack")) 54 | if err != nil { 55 | t.Fatalf("Error creating alternator load balancer: %v", err) 56 | } 57 | defer lb.Stop() 58 | 59 | if lb.CheckIfRackAndDatacenterSetCorrectly() == nil { 60 | t.Fatalf("CheckIfRackAndDatacenterSetCorrectly() should have returned an error") 61 | } 62 | } 63 | 64 | func TestCheckIfRackAndDatacenterSetCorrectly_CorrectRack(t *testing.T) { 65 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithPort(httpPort), alb.WithDatacenter("datacenter1"), alb.WithRack("rack1")) 66 | if err != nil { 67 | t.Fatalf("Error creating alternator load balancer: %v", err) 68 | } 69 | defer lb.Stop() 70 | 71 | if err := lb.CheckIfRackAndDatacenterSetCorrectly(); err != nil { 72 | t.Fatalf("CheckIfRackAndDatacenterSetCorrectly() unexpectedly returned an error: %v", err) 73 | } 74 | } 75 | 76 | func TestCheckIfRackDatacenterFeatureIsSupported(t *testing.T) { 77 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithPort(httpPort), alb.WithDatacenter("datacenter1")) 78 | if err != nil { 79 | t.Fatalf("Error creating alternator load balancer: %v", err) 80 | } 81 | defer lb.Stop() 82 | 83 | val, err := lb.CheckIfRackDatacenterFeatureIsSupported() 84 | if err != nil { 85 | t.Fatalf("CheckIfRackAndDatacenterSetCorrectly() unexpectedly returned an error: %v", err) 86 | } 87 | if !val { 88 | t.Fatalf("CheckIfRackAndDatacenterSetCorrectly() should have returned true") 89 | } 90 | } 91 | 92 | func TestDynamoDBOperations(t *testing.T) { 93 | t.Run("Plain", func(t *testing.T) { 94 | testDynamoDBOperations(t, alb.WithPort(httpPort)) 95 | }) 96 | t.Run("SSL", func(t *testing.T) { 97 | testDynamoDBOperations(t, alb.WithScheme("https"), alb.WithPort(httpsPort), alb.WithIgnoreServerCertificateError(true)) 98 | }) 99 | } 100 | 101 | type KeyWriter struct { 102 | keyData []byte 103 | } 104 | 105 | func (w *KeyWriter) Write(p []byte) (int, error) { 106 | w.keyData = append(w.keyData, p...) 107 | return len(p), nil 108 | } 109 | 110 | func TestKeyLogWriter(t *testing.T) { 111 | opts := []alb.Option{ 112 | alb.WithScheme("https"), 113 | alb.WithPort(httpsPort), 114 | alb.WithIgnoreServerCertificateError(true), 115 | alb.WithNodesListUpdatePeriod(0), 116 | alb.WithIdleNodesListUpdatePeriod(0), 117 | } 118 | t.Run("AlternatorLiveNodes", func(t *testing.T) { 119 | keyWriter := &KeyWriter{} 120 | lb, err := alb.NewAlternatorLB(knownNodes, append(slices.Clone(opts), alb.WithKeyLogWriter(keyWriter))...) 121 | if err != nil { 122 | t.Fatalf("Error creating alternator load balancer: %v", err) 123 | } 124 | defer lb.Stop() 125 | 126 | err = lb.UpdateLiveNodes() 127 | if err != nil { 128 | t.Fatalf("UpdateLiveNodes() unexpectedly returned an error: %v", err) 129 | } 130 | 131 | if len(keyWriter.keyData) == 0 { 132 | t.Fatalf("keyData should not be empty") 133 | } 134 | }) 135 | 136 | t.Run("DynamoDBAPI", func(t *testing.T) { 137 | keyWriter := &KeyWriter{} 138 | lb, err := alb.NewAlternatorLB(knownNodes, append(slices.Clone(opts), alb.WithKeyLogWriter(keyWriter))...) 139 | if err != nil { 140 | t.Fatalf("Error creating alternator load balancer: %v", err) 141 | } 142 | defer lb.Stop() 143 | 144 | ddb, err := lb.NewDynamoDB() 145 | if err != nil { 146 | t.Fatalf("Error creating dynamoDB client: %v", err) 147 | } 148 | 149 | _, _ = ddb.DeleteTable(&dynamodb.DeleteTableInput{ 150 | TableName: aws.String("table-that-does-not-exist"), 151 | }) 152 | 153 | if len(keyWriter.keyData) == 0 { 154 | t.Fatalf("keyData should not be empty") 155 | } 156 | }) 157 | } 158 | 159 | type sessionCache struct { 160 | orig tls.ClientSessionCache 161 | gets atomic.Uint32 162 | values map[string][][]byte 163 | valuesLock sync.Mutex 164 | } 165 | 166 | func (c *sessionCache) Get(sessionKey string) (session *tls.ClientSessionState, ok bool) { 167 | c.gets.Add(1) 168 | return c.orig.Get(sessionKey) 169 | } 170 | 171 | func (c *sessionCache) Put(sessionKey string, cs *tls.ClientSessionState) { 172 | c.valuesLock.Lock() 173 | ticket, _, err := cs.ResumptionState() 174 | if err != nil { 175 | panic(err) 176 | } 177 | if len(ticket) == 0 { 178 | panic("ticket should not be empty") 179 | } 180 | c.values[sessionKey] = append(c.values[sessionKey], ticket) 181 | c.valuesLock.Unlock() 182 | c.orig.Put(sessionKey, cs) 183 | } 184 | 185 | func (c *sessionCache) NumberOfTickets() int { 186 | c.valuesLock.Lock() 187 | defer c.valuesLock.Unlock() 188 | total := 0 189 | for _, tickets := range c.values { 190 | total += len(tickets) 191 | } 192 | return total 193 | } 194 | 195 | func newSessionCache() *sessionCache { 196 | return &sessionCache{ 197 | orig: tls.NewLRUClientSessionCache(10), 198 | values: make(map[string][][]byte), 199 | valuesLock: sync.Mutex{}, 200 | } 201 | } 202 | 203 | func TestTLSSessionCache(t *testing.T) { 204 | t.Skip("No scylla release available yet") 205 | 206 | opts := []alb.Option{ 207 | alb.WithScheme("https"), 208 | alb.WithPort(httpsPort), 209 | alb.WithIgnoreServerCertificateError(true), 210 | alb.WithNodesListUpdatePeriod(0), 211 | alb.WithIdleNodesListUpdatePeriod(0), 212 | alb.WithMaxIdleHTTPConnections(-1), // Make http client not to persist https connection 213 | } 214 | t.Run("AlternatorLiveNodes", func(t *testing.T) { 215 | cache := newSessionCache() 216 | lb, err := alb.NewAlternatorLB(knownNodes, append(slices.Clone(opts), alb.WithTLSSessionCache(cache))...) 217 | if err != nil { 218 | t.Fatalf("Error creating alternator load balancer: %v", err) 219 | } 220 | defer lb.Stop() 221 | 222 | err = lb.UpdateLiveNodes() 223 | if err != nil { 224 | t.Fatalf("UpdateLiveNodes() unexpectedly returned an error: %v", err) 225 | } 226 | 227 | tickets := cache.NumberOfTickets() 228 | if tickets == 0 { 229 | t.Fatalf("no session was learned") 230 | } 231 | 232 | err = lb.UpdateLiveNodes() 233 | if err != nil { 234 | t.Fatalf("UpdateLiveNodes() unexpectedly returned an error: %v", err) 235 | } 236 | 237 | if cache.NumberOfTickets() > tickets { 238 | t.Fatalf("session was not reused") 239 | } 240 | }) 241 | 242 | t.Run("DynamoDBAPI", func(t *testing.T) { 243 | cache := newSessionCache() 244 | lb, err := alb.NewAlternatorLB(knownNodes, append(slices.Clone(opts), alb.WithTLSSessionCache(cache))...) 245 | if err != nil { 246 | t.Fatalf("Error creating alternator load balancer: %v", err) 247 | } 248 | defer lb.Stop() 249 | 250 | ddb, err := lb.NewDynamoDB() 251 | if err != nil { 252 | t.Fatalf("Error creating dynamoDB client: %v", err) 253 | } 254 | 255 | _, err = ddb.DeleteTable(&dynamodb.DeleteTableInput{ 256 | TableName: aws.String("table-that-does-not-exist"), 257 | }) 258 | if err != nil && !errors.As(err, notFoundErr) { 259 | t.Fatalf("unexpected operation error: %v", err) 260 | } 261 | 262 | tickets := cache.NumberOfTickets() 263 | if tickets == 0 { 264 | t.Fatalf("no session was learned") 265 | } 266 | 267 | _, err = ddb.DeleteTable(&dynamodb.DeleteTableInput{ 268 | TableName: aws.String("table-that-does-not-exist"), 269 | }) 270 | if err != nil && !errors.As(err, notFoundErr) { 271 | t.Fatalf("unexpected operation error: %v", err) 272 | } 273 | 274 | if cache.NumberOfTickets() > tickets { 275 | t.Fatalf("session was not reused") 276 | } 277 | }) 278 | } 279 | 280 | func testDynamoDBOperations(t *testing.T, opts ...alb.Option) { 281 | t.Helper() 282 | 283 | const tableName = "test_table" 284 | lb, err := alb.NewAlternatorLB(knownNodes, opts...) 285 | if err != nil { 286 | t.Fatalf("Error creating alternator load balancer: %v", err) 287 | } 288 | defer lb.Stop() 289 | 290 | ddb, err := lb.WithCredentials("whatever", "secret").NewDynamoDB() 291 | if err != nil { 292 | t.Fatalf("Error creating dynamoDB client: %v", err) 293 | } 294 | 295 | _, _ = ddb.DeleteTable(&dynamodb.DeleteTableInput{ 296 | TableName: aws.String(tableName), 297 | }) 298 | 299 | _, err = ddb.CreateTable( 300 | &dynamodb.CreateTableInput{ 301 | TableName: aws.String(tableName), 302 | KeySchema: []*dynamodb.KeySchemaElement{ 303 | { 304 | AttributeName: aws.String("ID"), 305 | KeyType: aws.String("HASH"), 306 | }, 307 | }, 308 | AttributeDefinitions: []*dynamodb.AttributeDefinition{ 309 | { 310 | AttributeName: aws.String("ID"), 311 | AttributeType: aws.String("S"), 312 | }, 313 | }, 314 | ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ 315 | ReadCapacityUnits: aws.Int64(1), 316 | WriteCapacityUnits: aws.Int64(1), 317 | }, 318 | }) 319 | if err != nil { 320 | t.Fatalf("Error creating a table: %v", err) 321 | } 322 | 323 | _, err = ddb.PutItem( 324 | &dynamodb.PutItemInput{ 325 | TableName: aws.String(tableName), 326 | Item: map[string]*dynamodb.AttributeValue{ 327 | "ID": {S: aws.String("123")}, 328 | "Data": {S: aws.String("data")}, 329 | }, 330 | }) 331 | if err != nil { 332 | t.Fatalf("Error creating table record: %v", err) 333 | } 334 | 335 | result, err := ddb.GetItem( 336 | &dynamodb.GetItemInput{ 337 | TableName: aws.String(tableName), 338 | Key: map[string]*dynamodb.AttributeValue{ 339 | "ID": {S: aws.String("123")}, 340 | }, 341 | }) 342 | if err != nil { 343 | t.Fatalf("Error creating alternator load balancer: %v", err) 344 | } 345 | if result.Item == nil { 346 | t.Fatalf("no item found for table %s", tableName) 347 | } 348 | 349 | _, err = ddb.DeleteItem( 350 | &dynamodb.DeleteItemInput{ 351 | TableName: aws.String(tableName), 352 | Key: map[string]*dynamodb.AttributeValue{ 353 | "ID": {S: aws.String("123")}, 354 | }, 355 | }) 356 | if err != nil { 357 | t.Fatalf("Error deleting item: %v", err) 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /go/v1/go.mod: -------------------------------------------------------------------------------- 1 | module alternator_loadbalancing 2 | 3 | go 1.22.12 4 | 5 | require ( 6 | common v0.0.0-00010101000000-000000000000 7 | github.com/aws/aws-sdk-go v1.55.6 8 | ) 9 | 10 | require github.com/jmespath/go-jmespath v0.4.0 // indirect 11 | 12 | replace common => ./../common 13 | -------------------------------------------------------------------------------- /go/v1/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= 2 | github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 6 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 7 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 8 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 14 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 15 | -------------------------------------------------------------------------------- /go/v2/README.md: -------------------------------------------------------------------------------- 1 | # Alternator - Client-side load balancing - Go 2 | 3 | ## Introduction 4 | 5 | As explained in the [toplevel README](../../README.md), DynamoDB applications 6 | are usually aware of a _single endpoint_, a single URL to which they 7 | connect - e.g., `http://dynamodb.us-east-1.amazonaws.com`. But Alternator 8 | is distributed over a cluster of nodes, and we would like the application to 9 | send requests to all these nodes - not just to one. This is important for two 10 | reasons: **high availability** (the failure of a single Alternator node should 11 | not prevent the client from proceeding) and **load balancing** over all 12 | Alternator nodes. 13 | 14 | One of the ways to do this is to provide a modified library, which will 15 | allow a mostly-unmodified application which is only aware of one 16 | "endpoint URL" to send its requests to many different Alternator nodes. 17 | 18 | Our intention is _not_ to fork the existing AWS client library for Go. 19 | Rather, our intention is to provide a small library which tacks on to 20 | the existing `aws-sdk-go-v2` library which the application is already using, 21 | and makes it do the right thing for Alternator. 22 | 23 | ## The library 24 | 25 | The `AlternatorLB` class defined in `alternator_lb.go` can be used to 26 | easily change any application using `aws-sdk-go-v2` from using Amazon DynamoDB 27 | to use Alternator: While DynamoDB only has one "endpoint", this class helps 28 | us balance the requests between all the nodes in the Alternator cluster. 29 | 30 | ## Using the library 31 | 32 | You create a regular `dynamodb.Client` client by one of the methods listed below and 33 | the rest of the application can use this dynamodb client normally 34 | this `db` object is thread-safe and can be used from multiple threads. 35 | 36 | This client will send requests to an Alternator nodes, instead of AWS DynamoDB. 37 | 38 | Every request performed on patched session will pick a different live 39 | Alternator node to send it to. Despite us sending different requests 40 | to different nodes, Go will keep these connections cached and reuse them 41 | when we send another request to the same node. 42 | 43 | ### Rack and Datacenter awareness 44 | 45 | You can configure load balancer to target particular datacenter or rack via `WithRack` and `WithDatacenter` options, like so: 46 | ```golang 47 | lb, err := alb.NewAlternatorLB([]string{"x.x.x.x"}, alb.WithRack("someRack"), alb.WithDatacenter("someDc1")) 48 | ``` 49 | 50 | Additionally, you can check if alternator cluster know targeted rack/datacenter: 51 | ```golang 52 | if err := lb.CheckIfRackAndDatacenterSetCorrectly(); err != nil { 53 | return fmt.Errorf("CheckIfRackAndDatacenterSetCorrectly() unexpectedly returned an error: %v", err) 54 | } 55 | ``` 56 | 57 | To check if cluster support datacenter/rack feature supported you can call `CheckIfRackDatacenterFeatureIsSupported`: 58 | ```golang 59 | supported, err := lb.CheckIfRackDatacenterFeatureIsSupported() 60 | if err != nil { 61 | return fmt.Errorf("failed to check if rack/dc feature is supported: %v", err) 62 | } 63 | if !supported { 64 | return fmt.Errorf("dc/rack feature is not supporte") 65 | } 66 | ``` 67 | 68 | ### Spawn `dynamodb.DynamoDB` 69 | 70 | ```golang 71 | import ( 72 | "fmt" 73 | alb "alternator_loadbalancing_v2" 74 | ) 75 | 76 | func main() { 77 | lb, err := alternator_loadbalancing_v2.NewAlternatorLB([]string{"x.x.x.x"}, ) 78 | if err != nil { 79 | panic(fmt.Sprintf("Error creating alternator load balancer: %v", err)) 80 | } 81 | ddb := lb.WithCredentials("whatever", "secret").NewDynamoDB() 82 | if err != nil { 83 | panic(fmt.Sprintf("Error creating dynamoDB client: %v", err)) 84 | } 85 | 86 | ctx := context.Background() 87 | _, _ = ddb.DeleteTable(ctx, &dynamodb.DeleteTableInput{ 88 | TableName: aws.String(tableName), 89 | }) 90 | } 91 | ``` 92 | 93 | ## Decrypting TLS 94 | 95 | Read wireshark wiki regarding decrypting TLS traffic: https://wiki.wireshark.org/TLS#using-the-pre-master-secret 96 | In order to obtain pre master key secrets, you need to provide a file writer into `alb.WithKeyLogWriter`, example: 97 | 98 | ```go 99 | keyWriter, err := os.OpenFile("/tmp/pre-master-key.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 100 | if err != nil { 101 | panic("Error opening key writer: " + err.Error()) 102 | } 103 | defer keyWriter.Close() 104 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithScheme("https"), alb.WithPort(httpsPort), alb.WithIgnoreServerCertificateError(true), alb.WithKeyLogWriter(keyWriter)) 105 | ``` 106 | 107 | Then you need to configure your traffic analyzer to read pre master key secrets from this file. 108 | 109 | ## Example 110 | 111 | You can find examples in `[alternator_lb_test.go](alternator_lb_test.go)` -------------------------------------------------------------------------------- /go/v2/alternator_lb.go: -------------------------------------------------------------------------------- 1 | package alternator_loadbalancing_v2 2 | 3 | import ( 4 | "common" 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/credentials" 12 | "github.com/aws/aws-sdk-go-v2/service/dynamodb" 13 | 14 | smithyendpoints "github.com/aws/smithy-go/endpoints" 15 | ) 16 | 17 | type Option = common.Option 18 | 19 | var ( 20 | WithScheme = common.WithScheme 21 | WithPort = common.WithPort 22 | WithRack = common.WithRack 23 | WithDatacenter = common.WithDatacenter 24 | WithAWSRegion = common.WithAWSRegion 25 | WithNodesListUpdatePeriod = common.WithNodesListUpdatePeriod 26 | WithIdleNodesListUpdatePeriod = common.WithIdleNodesListUpdatePeriod 27 | WithCredentials = common.WithCredentials 28 | WithHTTPClient = common.WithHTTPClient 29 | WithLocalNodesReaderHTTPClient = common.WithLocalNodesReaderHTTPClient 30 | WithClientCertificateFile = common.WithClientCertificateFile 31 | WithClientCertificate = common.WithClientCertificate 32 | WithClientCertificateSource = common.WithClientCertificateSource 33 | WithIgnoreServerCertificateError = common.WithIgnoreServerCertificateError 34 | WithOptimizeHeaders = common.WithOptimizeHeaders 35 | WithKeyLogWriter = common.WithKeyLogWriter 36 | WithTLSSessionCache = common.WithTLSSessionCache 37 | WithMaxIdleHTTPConnections = common.WithMaxIdleHTTPConnections 38 | ) 39 | 40 | type AlternatorLB struct { 41 | nodes *common.AlternatorLiveNodes 42 | cfg common.Config 43 | } 44 | 45 | func NewAlternatorLB(initialNodes []string, options ...common.Option) (*AlternatorLB, error) { 46 | cfg := common.NewConfig() 47 | for _, opt := range options { 48 | opt(cfg) 49 | } 50 | 51 | nodes, err := common.NewAlternatorLiveNodes(initialNodes, cfg.ToALNOptions()...) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return &AlternatorLB{ 56 | nodes: nodes, 57 | cfg: *cfg, 58 | }, nil 59 | } 60 | 61 | // AWSConfig produces a conf for the AWS SDK that will integrate the alternator loadbalancing with the AWS SDK. 62 | func (lb *AlternatorLB) AWSConfig() (aws.Config, error) { 63 | cfg := aws.Config{ 64 | // Region is used in the signature algorithm so prevent request sent 65 | // to one region to be forward by an attacker to a different region. 66 | // But Alternator doesn't check it. It can be anything. 67 | Region: lb.cfg.AWSRegion, 68 | BaseEndpoint: aws.String(fmt.Sprintf("%s://%s:%d", lb.cfg.Scheme, "dynamodb.fake.alterntor.cluster.node", lb.cfg.Port)), 69 | } 70 | 71 | if lb.cfg.HTTPClient != nil { 72 | cfg.HTTPClient = lb.cfg.HTTPClient 73 | } else { 74 | cfg.HTTPClient = &http.Client{ 75 | Transport: common.DefaultHTTPTransport(), 76 | } 77 | } 78 | 79 | err := common.PatchHTTPClient(lb.cfg, cfg.HTTPClient) 80 | if err != nil { 81 | return aws.Config{}, err 82 | } 83 | 84 | if lb.cfg.AccessKeyID != "" && lb.cfg.SecretAccessKey != "" { 85 | // The third credential below, the session token, is only used for 86 | // temporary credentials, and is not supported by Alternator anyway. 87 | cfg.Credentials = credentials.NewStaticCredentialsProvider(lb.cfg.AccessKeyID, lb.cfg.SecretAccessKey, "") 88 | } 89 | 90 | return cfg, nil 91 | } 92 | 93 | // WithCredentials creates clone of AlternatorLB with altered alternator credentials 94 | func (lb *AlternatorLB) WithCredentials(accessKeyID, secretAccessKey string) *AlternatorLB { 95 | cfg := lb.cfg 96 | common.WithCredentials(accessKeyID, secretAccessKey)(&cfg) 97 | return &AlternatorLB{ 98 | nodes: lb.nodes, 99 | cfg: cfg, 100 | } 101 | } 102 | 103 | // WithAWSRegion creates clone of AlternatorLB with altered AWS region 104 | func (lb *AlternatorLB) WithAWSRegion(region string) *AlternatorLB { 105 | cfg := lb.cfg 106 | common.WithAWSRegion(region)(&cfg) 107 | return &AlternatorLB{ 108 | nodes: lb.nodes, 109 | cfg: cfg, 110 | } 111 | } 112 | 113 | func (lb *AlternatorLB) NextNode() url.URL { 114 | return lb.nodes.NextNode() 115 | } 116 | 117 | func (lb *AlternatorLB) UpdateLiveNodes() error { 118 | return lb.nodes.UpdateLiveNodes() 119 | } 120 | 121 | func (lb *AlternatorLB) CheckIfRackAndDatacenterSetCorrectly() error { 122 | return lb.nodes.CheckIfRackAndDatacenterSetCorrectly() 123 | } 124 | 125 | func (lb *AlternatorLB) CheckIfRackDatacenterFeatureIsSupported() (bool, error) { 126 | return lb.nodes.CheckIfRackDatacenterFeatureIsSupported() 127 | } 128 | 129 | func (lb *AlternatorLB) Start() { 130 | lb.nodes.Start() 131 | } 132 | 133 | func (lb *AlternatorLB) Stop() { 134 | lb.nodes.Stop() 135 | } 136 | 137 | func (lb *AlternatorLB) endpointResolverV2() dynamodb.EndpointResolverV2 { 138 | return &EndpointResolverV2{lb: lb} 139 | } 140 | 141 | func (lb *AlternatorLB) NewDynamoDB() (*dynamodb.Client, error) { 142 | cfg, err := lb.AWSConfig() 143 | if err != nil { 144 | return nil, err 145 | } 146 | return dynamodb.NewFromConfig(cfg, dynamodb.WithEndpointResolverV2(lb.endpointResolverV2())), nil 147 | } 148 | 149 | type EndpointResolverV2 struct { 150 | lb *AlternatorLB 151 | } 152 | 153 | func (r *EndpointResolverV2) ResolveEndpoint(_ context.Context, _ dynamodb.EndpointParameters) (smithyendpoints.Endpoint, error) { 154 | return smithyendpoints.Endpoint{ 155 | URI: r.lb.NextNode(), 156 | }, nil 157 | } 158 | -------------------------------------------------------------------------------- /go/v2/alternator_lb_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package alternator_loadbalancing_v2_test 5 | 6 | import ( 7 | "context" 8 | "crypto/tls" 9 | "errors" 10 | "slices" 11 | "sync" 12 | "sync/atomic" 13 | "testing" 14 | 15 | "github.com/aws/smithy-go" 16 | 17 | "github.com/aws/aws-sdk-go-v2/aws" 18 | "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" 19 | "github.com/aws/aws-sdk-go-v2/service/dynamodb" 20 | "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" 21 | 22 | alb "alternator_loadbalancing_v2" 23 | ) 24 | 25 | var ( 26 | knownNodes = []string{"172.41.0.2"} 27 | httpsPort = 9999 28 | httpPort = 9998 29 | ) 30 | 31 | var notFoundErr = new(*smithy.OperationError) 32 | 33 | func TestCheckIfRackAndDatacenterSetCorrectly_WrongDC(t *testing.T) { 34 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithPort(httpPort), alb.WithDatacenter("wrongDC")) 35 | if err != nil { 36 | t.Errorf("Error creating alternator load balancer: %v", err) 37 | } 38 | defer lb.Stop() 39 | 40 | if lb.CheckIfRackAndDatacenterSetCorrectly() == nil { 41 | t.Errorf("CheckIfRackAndDatacenterSetCorrectly() should have returned an error") 42 | } 43 | } 44 | 45 | func TestCheckIfRackAndDatacenterSetCorrectly_CorrectDC(t *testing.T) { 46 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithPort(httpPort), alb.WithDatacenter("datacenter1")) 47 | if err != nil { 48 | t.Errorf("Error creating alternator load balancer: %v", err) 49 | } 50 | defer lb.Stop() 51 | 52 | if err := lb.CheckIfRackAndDatacenterSetCorrectly(); err != nil { 53 | t.Errorf("CheckIfRackAndDatacenterSetCorrectly() unexpectedly returned an error: %v", err) 54 | } 55 | } 56 | 57 | func TestCheckIfRackAndDatacenterSetCorrectly_WrongRack(t *testing.T) { 58 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithPort(httpPort), alb.WithDatacenter("wrongDC"), alb.WithRack("wrongRack")) 59 | if err != nil { 60 | t.Errorf("Error creating alternator load balancer: %v", err) 61 | } 62 | defer lb.Stop() 63 | 64 | if lb.CheckIfRackAndDatacenterSetCorrectly() == nil { 65 | t.Errorf("CheckIfRackAndDatacenterSetCorrectly() should have returned an error") 66 | } 67 | } 68 | 69 | func TestCheckIfRackAndDatacenterSetCorrectly_CorrectRack(t *testing.T) { 70 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithPort(httpPort), alb.WithDatacenter("datacenter1"), alb.WithRack("rack1")) 71 | if err != nil { 72 | t.Errorf("Error creating alternator load balancer: %v", err) 73 | } 74 | defer lb.Stop() 75 | 76 | if err := lb.CheckIfRackAndDatacenterSetCorrectly(); err != nil { 77 | t.Errorf("CheckIfRackAndDatacenterSetCorrectly() unexpectedly returned an error: %v", err) 78 | } 79 | } 80 | 81 | func TestCheckIfRackDatacenterFeatureIsSupported(t *testing.T) { 82 | lb, err := alb.NewAlternatorLB(knownNodes, alb.WithPort(httpPort), alb.WithDatacenter("datacenter1")) 83 | if err != nil { 84 | t.Errorf("Error creating alternator load balancer: %v", err) 85 | } 86 | defer lb.Stop() 87 | 88 | val, err := lb.CheckIfRackDatacenterFeatureIsSupported() 89 | if err != nil { 90 | t.Errorf("CheckIfRackAndDatacenterSetCorrectly() unexpectedly returned an error: %v", err) 91 | } 92 | if !val { 93 | t.Errorf("CheckIfRackAndDatacenterSetCorrectly() should have returned true") 94 | } 95 | } 96 | 97 | func TestDynamoDBOperations(t *testing.T) { 98 | t.Run("Plain", func(t *testing.T) { 99 | testDynamoDBOperations(t, alb.WithPort(httpPort)) 100 | }) 101 | t.Run("SSL", func(t *testing.T) { 102 | testDynamoDBOperations(t, alb.WithScheme("https"), alb.WithPort(httpsPort), alb.WithIgnoreServerCertificateError(true)) 103 | }) 104 | } 105 | 106 | type KeyWriter struct { 107 | keyData []byte 108 | } 109 | 110 | func (w *KeyWriter) Write(p []byte) (int, error) { 111 | w.keyData = append(w.keyData, p...) 112 | return len(p), nil 113 | } 114 | 115 | func TestKeyLogWriter(t *testing.T) { 116 | opts := []alb.Option{ 117 | alb.WithScheme("https"), 118 | alb.WithPort(httpsPort), 119 | alb.WithIgnoreServerCertificateError(true), 120 | alb.WithNodesListUpdatePeriod(0), 121 | alb.WithIdleNodesListUpdatePeriod(0), 122 | } 123 | t.Run("AlternatorLiveNodes", func(t *testing.T) { 124 | keyWriter := &KeyWriter{} 125 | lb, err := alb.NewAlternatorLB(knownNodes, append(slices.Clone(opts), alb.WithKeyLogWriter(keyWriter))...) 126 | if err != nil { 127 | t.Fatalf("Error creating alternator load balancer: %v", err) 128 | } 129 | defer lb.Stop() 130 | 131 | err = lb.UpdateLiveNodes() 132 | if err != nil { 133 | t.Fatalf("UpdateLiveNodes() unexpectedly returned an error: %v", err) 134 | } 135 | 136 | if len(keyWriter.keyData) == 0 { 137 | t.Fatalf("keyData should not be empty") 138 | } 139 | }) 140 | 141 | t.Run("DynamoDBAPI", func(t *testing.T) { 142 | keyWriter := &KeyWriter{} 143 | lb, err := alb.NewAlternatorLB(knownNodes, append(slices.Clone(opts), alb.WithKeyLogWriter(keyWriter))...) 144 | if err != nil { 145 | t.Fatalf("Error creating alternator load balancer: %v", err) 146 | } 147 | defer lb.Stop() 148 | 149 | ddb, err := lb.NewDynamoDB() 150 | if err != nil { 151 | t.Fatalf("Error creating dynamoDB client: %v", err) 152 | } 153 | 154 | _, err = ddb.DeleteTable(context.Background(), &dynamodb.DeleteTableInput{ 155 | TableName: aws.String("table-that-does-not-exist"), 156 | }) 157 | if err != nil && !errors.As(err, notFoundErr) { 158 | t.Fatalf("Error creating dynamoDB client: %v", err) 159 | } 160 | 161 | if len(keyWriter.keyData) == 0 { 162 | t.Fatalf("keyData should not be empty") 163 | } 164 | }) 165 | } 166 | 167 | type sessionCache struct { 168 | orig tls.ClientSessionCache 169 | gets atomic.Uint32 170 | values map[string][][]byte 171 | valuesLock sync.Mutex 172 | } 173 | 174 | func (c *sessionCache) Get(sessionKey string) (session *tls.ClientSessionState, ok bool) { 175 | c.gets.Add(1) 176 | return c.orig.Get(sessionKey) 177 | } 178 | 179 | func (c *sessionCache) Put(sessionKey string, cs *tls.ClientSessionState) { 180 | ticket, _, err := cs.ResumptionState() 181 | if err != nil { 182 | panic(err) 183 | } 184 | if len(ticket) == 0 { 185 | panic("ticket should not be empty") 186 | } 187 | c.valuesLock.Lock() 188 | c.values[sessionKey] = append(c.values[sessionKey], ticket) 189 | c.valuesLock.Unlock() 190 | c.orig.Put(sessionKey, cs) 191 | } 192 | 193 | func (c *sessionCache) NumberOfTickets() int { 194 | c.valuesLock.Lock() 195 | defer c.valuesLock.Unlock() 196 | total := 0 197 | for _, tickets := range c.values { 198 | total += len(tickets) 199 | } 200 | return total 201 | } 202 | 203 | func newSessionCache() *sessionCache { 204 | return &sessionCache{ 205 | orig: tls.NewLRUClientSessionCache(10), 206 | values: make(map[string][][]byte), 207 | valuesLock: sync.Mutex{}, 208 | } 209 | } 210 | 211 | func TestTLSSessionCache(t *testing.T) { 212 | t.Skip("No scylla release available yet") 213 | 214 | opts := []alb.Option{ 215 | alb.WithScheme("https"), 216 | alb.WithPort(httpsPort), 217 | alb.WithIgnoreServerCertificateError(true), 218 | alb.WithNodesListUpdatePeriod(0), 219 | alb.WithIdleNodesListUpdatePeriod(0), 220 | alb.WithMaxIdleHTTPConnections(-1), // Make http client not to persist https connection 221 | } 222 | 223 | t.Run("AlternatorLiveNodes", func(t *testing.T) { 224 | cache := newSessionCache() 225 | lb, err := alb.NewAlternatorLB(knownNodes, append(slices.Clone(opts), alb.WithTLSSessionCache(cache))...) 226 | if err != nil { 227 | t.Fatalf("Error creating alternator load balancer: %v", err) 228 | } 229 | defer lb.Stop() 230 | 231 | err = lb.UpdateLiveNodes() 232 | if err != nil { 233 | t.Fatalf("UpdateLiveNodes() unexpectedly returned an error: %v", err) 234 | } 235 | 236 | if len(cache.values) == 0 { 237 | t.Fatalf("no session was learned") 238 | } 239 | 240 | if len(cache.values) == 0 { 241 | t.Fatalf("no ticket was learned") 242 | } 243 | }) 244 | 245 | t.Run("DynamoDBAPI", func(t *testing.T) { 246 | cache := newSessionCache() 247 | lb, err := alb.NewAlternatorLB(knownNodes, append(slices.Clone(opts), alb.WithTLSSessionCache(cache))...) 248 | if err != nil { 249 | t.Fatalf("Error creating alternator load balancer: %v", err) 250 | } 251 | defer lb.Stop() 252 | 253 | ddb, err := lb.NewDynamoDB() 254 | if err != nil { 255 | t.Fatalf("Error creating dynamoDB client: %v", err) 256 | } 257 | 258 | _, err = ddb.DeleteTable(context.Background(), &dynamodb.DeleteTableInput{ 259 | TableName: aws.String("table-that-does-not-exist"), 260 | }) 261 | if err != nil && !errors.As(err, notFoundErr) { 262 | t.Fatalf("Error creating dynamoDB client: %v", err) 263 | } 264 | 265 | if len(cache.values) == 0 { 266 | t.Fatalf("no session was learned") 267 | } 268 | 269 | if len(cache.values) == 0 { 270 | t.Fatalf("no ticket was learned") 271 | } 272 | }) 273 | } 274 | 275 | func testDynamoDBOperations(t *testing.T, opts ...alb.Option) { 276 | t.Helper() 277 | 278 | const tableName = "test_table" 279 | lb, err := alb.NewAlternatorLB(knownNodes, opts...) 280 | if err != nil { 281 | t.Errorf("Error creating alternator load balancer: %v", err) 282 | } 283 | defer lb.Stop() 284 | 285 | ddb, err := lb.WithCredentials("whatever", "secret").NewDynamoDB() 286 | if err != nil { 287 | t.Errorf("Error creating dynamoDB client: %v", err) 288 | } 289 | 290 | ctx := context.Background() 291 | 292 | _, _ = ddb.DeleteTable(ctx, &dynamodb.DeleteTableInput{ 293 | TableName: aws.String(tableName), 294 | }) 295 | 296 | _, err = ddb.CreateTable( 297 | ctx, 298 | &dynamodb.CreateTableInput{ 299 | TableName: aws.String(tableName), 300 | KeySchema: []types.KeySchemaElement{ 301 | { 302 | AttributeName: aws.String("ID"), 303 | KeyType: "HASH", 304 | }, 305 | }, 306 | AttributeDefinitions: []types.AttributeDefinition{ 307 | { 308 | AttributeName: aws.String("ID"), 309 | AttributeType: "S", 310 | }, 311 | }, 312 | ProvisionedThroughput: &types.ProvisionedThroughput{ 313 | ReadCapacityUnits: aws.Int64(1), 314 | WriteCapacityUnits: aws.Int64(1), 315 | }, 316 | }) 317 | if err != nil { 318 | t.Fatalf("Error creating a table: %v", err) 319 | } 320 | 321 | val, err := attributevalue.MarshalMap(map[string]interface{}{ 322 | "ID": "123", 323 | "Name": "value", 324 | }) 325 | if err != nil { 326 | t.Fatalf("Error marshalling item: %v", err) 327 | } 328 | 329 | key, err := attributevalue.Marshal("123") 330 | if err != nil { 331 | t.Fatalf("Error marshalling item: %v", err) 332 | } 333 | 334 | _, err = ddb.PutItem( 335 | ctx, 336 | &dynamodb.PutItemInput{ 337 | TableName: aws.String(tableName), 338 | Item: val, 339 | }) 340 | if err != nil { 341 | t.Fatalf("Error creating table record: %v", err) 342 | } 343 | 344 | result, err := ddb.GetItem( 345 | ctx, 346 | &dynamodb.GetItemInput{ 347 | TableName: aws.String(tableName), 348 | Key: map[string]types.AttributeValue{ 349 | "ID": key, 350 | }, 351 | }) 352 | if err != nil { 353 | t.Fatalf("Error creating a record: %v", err) 354 | } 355 | if result.Item == nil { 356 | t.Errorf("no item found") 357 | } 358 | 359 | _, err = ddb.DeleteItem( 360 | ctx, 361 | &dynamodb.DeleteItemInput{ 362 | TableName: aws.String(tableName), 363 | Key: map[string]types.AttributeValue{ 364 | "ID": key, 365 | }, 366 | }) 367 | if err != nil { 368 | t.Errorf("Error deleting item: %v", err) 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /go/v2/go.mod: -------------------------------------------------------------------------------- 1 | module alternator_loadbalancing_v2 2 | 3 | go 1.22.12 4 | 5 | require ( 6 | common v0.0.0-00010101000000-000000000000 7 | github.com/aws/aws-sdk-go-v2 v1.36.3 8 | github.com/aws/aws-sdk-go-v2/credentials v1.17.62 9 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.18.7 10 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.41.1 11 | github.com/aws/smithy-go v1.22.3 12 | ) 13 | 14 | require ( 15 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 16 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 17 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.1 // indirect 18 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 19 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.15 // indirect 20 | ) 21 | 22 | replace common => ./../common 23 | -------------------------------------------------------------------------------- /go/v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.36.2 h1:Ub6I4lq/71+tPb/atswvToaLGVMxKZvjYDVOWEExOcU= 2 | github.com/aws/aws-sdk-go-v2 v1.36.2/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 3 | github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 4 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 5 | github.com/aws/aws-sdk-go-v2/config v1.29.7 h1:71nqi6gUbAUiEQkypHQcNVSFJVUFANpSeUNShiwWX2M= 6 | github.com/aws/aws-sdk-go-v2/config v1.29.7/go.mod h1:yqJQ3nh2HWw/uxd56bicyvmDW4KSc+4wN6lL8pYjynU= 7 | github.com/aws/aws-sdk-go-v2/credentials v1.17.60 h1:1dq+ELaT5ogfmqtV1eocq8SpOK1NRsuUfmhQtD/XAh4= 8 | github.com/aws/aws-sdk-go-v2/credentials v1.17.60/go.mod h1:HDes+fn/xo9VeszXqjBVkxOo/aUy8Mc6QqKvZk32GlE= 9 | github.com/aws/aws-sdk-go-v2/credentials v1.17.61 h1:Hd/uX6Wo2iUW1JWII+rmyCD7MMhOe7ALwQXN6sKDd1o= 10 | github.com/aws/aws-sdk-go-v2/credentials v1.17.61/go.mod h1:L7vaLkwHY1qgW0gG1zG0z/X0sQ5tpIY5iI13+j3qI80= 11 | github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= 12 | github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8= 13 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.18.5 h1:3QhGiWUMeyuAPT4RjIwNEQUu+wOmPLLOz8IpgiiKWM4= 14 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.18.5/go.mod h1:J+3D/6T2wbhBJkv7BevC/8QV3GSHYrTKK30EWqWtJkU= 15 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.18.6 h1:5MXQb+ASlUe0SgSmPt8V0l4EFRKLyr0krAnMqMvlAjQ= 16 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.18.6/go.mod h1:V+IXONaymKaUpRMGVqdjaXhZwYFHAgFwxmJi6/132tE= 17 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.18.7 h1:XUU8kEvb2hJd2z5uu/opq3byWwPrl9wH/jsVTWJ7IhM= 18 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.18.7/go.mod h1:mLzHwUsn6O03hXf0wNhEy1ICdDdDBnCPdWlM3t63aQo= 19 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29 h1:JO8pydejFKmGcUNiiwt75dzLHRWthkwApIvPoyUtXEg= 20 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29/go.mod h1:adxZ9i9DRmB8zAT0pO0yGnsmu0geomp5a3uq5XpgOJ8= 21 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 22 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 h1:knLyPMw3r3JsU8MFHWctE4/e2qWbPaxDYLlohPvnY8c= 23 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33/go.mod h1:EBp2HQ3f+XCB+5J+IoEbGhoV7CpJbnrsd4asNXmTL0A= 24 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 25 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 26 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 h1:K0+Ne08zqti8J9jwENxZ5NoUyBnaFDTu3apwQJWrwwA= 27 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33/go.mod h1:K97stwwzaWzmqxO8yLGHhClbVW1tC6VT1pDLk1pGrq4= 28 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 29 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 30 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 31 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 32 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.40.2 h1:lT4US8VW4CAsCzJy0JpH/vPuJD9nG/73ioLHDlKQDU8= 33 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.40.2/go.mod h1:QwexjOlSUV85+ct6LohHmsaFTiW2j1s+9SQZNVjhAV0= 34 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.41.0 h1:kSMAk72LZ5eIdY/W+tVV6VdokciajcDdVClEBVNWNP0= 35 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.41.0/go.mod h1:yYaWRnVSPyAmexW5t7G3TcuYoalYfT+xQwzWsvtUQ7M= 36 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.41.1 h1:DEys4E5Q2p735j56lteNVyByIBDAlMrO5VIEd9RC0/4= 37 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.41.1/go.mod h1:yYaWRnVSPyAmexW5t7G3TcuYoalYfT+xQwzWsvtUQ7M= 38 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.21 h1:6uTJJuQouHbWupYOhgCY3v6xZP1VbJlHQsiFqwVdebY= 39 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.21/go.mod h1:isd8r8zEUafc7PBf+Z2QwCgbku0xYL0/ea8EI9u1AGo= 40 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.0 h1:iTFqGH+Eel+KPW0cFvsA6JVP9/86MEbENVz60dbHxIs= 41 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.0/go.mod h1:lUqWdw5/esjPTkITXhN4C66o1ltwDq2qQ12j3SOzhVg= 42 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.1 h1:ZJfy2cSyoAOl7maGfRI4/J+cy00AczaYwVCow+bsc4k= 43 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.1/go.mod h1:lUqWdw5/esjPTkITXhN4C66o1ltwDq2qQ12j3SOzhVg= 44 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 45 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 46 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.14 h1:a4cztfjtvD/DDPxWzRnMskxeEVgEXUYAFHBFz+eVjIc= 47 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.14/go.mod h1:4Z0HHlXIU+k510CCfnTtgUon5MMymnSAOp9i0/nLfpA= 48 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.15 h1:M1R1rud7HzDrfCdlBQ7NjnRsDNEhXO/vGhuD189Ggmk= 49 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.15/go.mod h1:uvFKBSq9yMPV4LGAi7N4awn4tLY+hKE35f8THes2mzQ= 50 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14 h1:2scbY6//jy/s8+5vGrk7l1+UtHl0h9A4MjOO2k/TM2E= 51 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14/go.mod h1:bRpZPHZpSe5YRHmPfK3h1M7UBFCn2szHzyx0rw04zro= 52 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 53 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.16 h1:YV6xIKDJp6U7YB2bxfud9IENO1LRpGhe2Tv/OKtPrOQ= 54 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.16/go.mod h1:DvbmMKgtpA6OihFJK13gHMZOZrCHttz8wPHGKXqU+3o= 55 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.0 h1:2U9sF8nKy7UgyEeLiZTRg6ShBS22z8UnYpV6aRFL0is= 56 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= 57 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15 h1:kMyK3aKotq1aTBsj1eS8ERJLjqYRRRcsmP33ozlCvlk= 58 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15/go.mod h1:5uPZU7vSNzb8Y0dm75xTikinegPYK3uJmIHQZFq5Aqo= 59 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.0 h1:wjAdc85cXdQR5uLx5FwWvGIHm4OPJhTyzUHU8craXtE= 60 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= 61 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.15 h1:ht1jVmeeo2anR7zDiYJLSnRYnO/9NILXXu42FP3rJg0= 62 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.15/go.mod h1:xWZ5cOiFe3czngChE4LhCBqUxNwgfwndEF7XlYP/yD8= 63 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.16 h1:BHEK2Q/7CMRMCb3nySi/w8UbIcPhKvYP5s1xf8/izn0= 64 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= 65 | github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= 66 | github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 67 | github.com/scylladb/alternator-load-balancing/go/v2 v2.0.0-20250130003930-ba0d60499ba6 h1:L5B1UwW79L3dE2B9CL+7TdB+mhg2SPm4oPZ8jxJqhg0= 68 | github.com/scylladb/alternator-load-balancing/go/v2 v2.0.0-20250130003930-ba0d60499ba6/go.mod h1:hkESD3Xapz9GfxeWYvIkd9o+cLJJh2jRHHUbzIKJdZ8= 69 | -------------------------------------------------------------------------------- /java/Makefile: -------------------------------------------------------------------------------- 1 | mvn = mvn 2 | 3 | ifdef IS_CICD 4 | mvn = mvn --no-transfer-progress 5 | endif 6 | 7 | 8 | clean: 9 | $mvn clean 10 | 11 | verify: 12 | ${mvn} verify 13 | ${mvn} javadoc:test-javadoc javadoc:test-aggregate javadoc:test-aggregate-jar javadoc:test-jar javadoc:test-resource-bundle 14 | ${mvn} javadoc:jar javadoc:aggregate javadoc:aggregate-jar javadoc:resource-bundle 15 | 16 | fix: 17 | ${mvn} com.coveo:fmt-maven-plugin:format 18 | echo y | ${mvn} javadoc:fix 19 | echo y | ${mvn} javadoc:test-fix 20 | 21 | compile: 22 | ${mvn} compile 23 | 24 | compile-test: 25 | ${mvn} test-compile 26 | -------------------------------------------------------------------------------- /java/README.md: -------------------------------------------------------------------------------- 1 | # Alternator - Client-side load balancing - Java 2 | 3 | ## Introduction 4 | As explained in the [toplevel README](../README.md), DynamoDB applications 5 | are usually aware of a _single endpoint_, a single URL to which they 6 | connect - e.g., `http://dynamodb.us-east-1.amazonaws.com`. But Alternator 7 | is distributed over a cluster of nodes and we would like the application to 8 | send requests to all these nodes - not just to one. This is important for two 9 | reasons: **high availability** (the failure of a single Alternator node should 10 | not prevent the client from proceeding) and **load balancing** over all 11 | Alternator nodes. 12 | 13 | One of the ways to do this is to provide a modified library, which will 14 | allow a mostly-unmodified application which is only aware of one 15 | "enpoint URL" to send its requests to many different Alternator nodes. 16 | 17 | Our intention is _not_ to fork the existing AWS client library (SDK) for Java. 18 | Rather, our intention is to provide a tiny library which tacks on to any 19 | version of the AWS SDK that the application is already using, and makes 20 | it do the right thing for Alternator. 21 | 22 | AWS SDK for Java has two distinct versions: Version 1 and Version 2. 23 | Version 2 is a complete rewrite of Version 1, with a completely different 24 | API. It was released in 2017, and announced in the following post: 25 | https://aws.amazon.com/blogs/developer/aws-sdk-for-java-2-0-developer-preview/ 26 | However, although Amazon recommend version 2 for new applications, both 27 | versions are still in popular use today, so the Alternator load balancing 28 | library described here supports both (our version 2 support requires 2.20 29 | or above). 30 | 31 | ## Add `load-balancing` to your project 32 | 33 | ### Maven Dependency 34 | 35 | Add the `load-balancing` dependency to your Maven project by adding the 36 | following `dependency` to your `pom.xml` definition: 37 | 38 | ~~~ xml 39 | 40 | com.scylladb.alternator 41 | load-balancing 42 | 1.0.0 43 | 44 | ~~~ 45 | 46 | You can find the latest version [here](https://central.sonatype.com/artifact/com.scylladb.alternator/load-balancing). 47 | 48 | ### Alternatively, build the LoadBalancing jar 49 | To build a jar of the Alternator client-side load balancer, use 50 | ``` 51 | mvn package 52 | ``` 53 | Which creates `target/load-balancing-1.0.0-SNAPSHOT.jar`. 54 | 55 | ## Usage 56 | 57 | As explained above, this package does not _replace_ the AWS SDK for Java, but 58 | accompanies it, and either version 1 or 2 of the AWS SDK for Java can be 59 | used (the details on how to use are slightly different for each version, 60 | so will be explained in separate sections below). 61 | 62 | As we show below, the package provides a new mechanism to configure a 63 | DynamoDB (v1) or DynamoDbClient (v2) object, which the application 64 | can then use normally using the standard AWS SDK for Java, to make 65 | requests. The load balancer library ensures that each of these requests 66 | goes to a different live Alternator node. 67 | 68 | The load balancer library is also responsible for _discovering_ which 69 | Alternator nodes exist, and maintaining this list as the Alternator cluster 70 | changes. It does this using an additional background thread, which 71 | periodically polls one of the known nodes, asking it for a list of all other 72 | nodes (in this data-center). 73 | 74 | ### Using the library, in AWS SDK for Java v1 75 | 76 | An application using AWS SDK for Java v1 creates a `DynamoDB` object and then 77 | uses it to perform various requests. Traditionally, to create such an object, 78 | an application that wishes to connect to a specific URL would use code that 79 | looks something like this: 80 | 81 | ```java 82 | AWSCredentialsProvider myCredentials = 83 | new AWSStaticCredentialsProvider(new BasicAWSCredentials("myusername", "mypassword")); 84 | 85 | AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard() 86 | .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration( 87 | "https://127.0.0.1:8043/", "region-doesnt-matter")) 88 | .withCredentials(myCredentials) 89 | .build(); 90 | DynamoDB dynamodb = new DynamoDB(client); 91 | ``` 92 | 93 | To use the Alternator load balancer and all Alternator nodes, all you need to 94 | do is tell the DynamoDB client to use a special request handler. The new code 95 | will look like this: 96 | 97 | ```java 98 | import com.scylladb.alternator.AlternatorRequestHandler; 99 | 100 | URI uri = URI.create("https://127.0.0.1:8043/"); 101 | AlternatorRequestHandler handler = new AlternatorRequestHandler(uri); 102 | AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard() 103 | .withRegion("region-doesnt-matter") 104 | .withRequestHandlers(handler) 105 | .withCredentials(myCredentials) 106 | .build(); 107 | DynamoDB dynamodb = new DynamoDB(client); 108 | ``` 109 | 110 | The application can then use this `DynamoDB` object completely normally, just 111 | that each request will go to a different Alternator node, instead of all of 112 | them going to the same URL. 113 | 114 | The main novelty in the new code above is the `AlternatorRequestHandler`, 115 | which is passed into the client builder with `withRequestHandlers(handler)`. 116 | The application should keep just one `AlternatorRequestHandler` object, as it 117 | keeps a background thread maintaining the list of live Alternator nodes, and 118 | it is pointless to have more than one of these threads. 119 | 120 | The parameter `uri` one known Alternator node, which is then contacted to 121 | discover the rest. After this initialization, this original node may go down 122 | at any time - any other already-known node can be used to retrieve the node 123 | list, and we no longer rely on the original node. 124 | 125 | The region passed to `withRegion` does not matter (and can be any string), 126 | because `AlternatorRequestHandler` will override the chosen endpoint anyway, 127 | Unfortunately we can't just drop the `withRegion()` call, because without 128 | it the library will expect to find a default region in the configuration file 129 | and complain when it is missing. 130 | 131 | You can see `src/test/java/com/scylladb/alternator/test/Demo1.java` for a 132 | complete example of using client-side load balancing with AWS SDK for Java v1. 133 | After building with `mvn package`, you can run this demo with the command: 134 | ``` 135 | mvn exec:java -Dexec.mainClass=com.scylladb.alternator.test.Demo1 -Dexec.classpathScope=test 136 | ``` 137 | 138 | ### Using the library, in AWS SDK for Java v2 139 | 140 | An application using AWS SDK for Java v2 creates a `DynamoDbClient` object 141 | and then uses it to perform various requests. Traditionally, to create such 142 | an object, an application that wishes to connect to a specific URL would use 143 | code that looks something like this: 144 | 145 | ```java 146 | static AwsCredentialsProvider myCredentials = 147 | StaticCredentialsProvider.create(AwsBasicCredentials.create("myuser", "mypassword")); 148 | URI uri = URI.create("https://127.0.0.1:8043"); 149 | DynamoDbClient client = DynamoDbClient.builder() 150 | .region(Region.US_EAST_1) 151 | .endpointOverride(url) 152 | .credentialsProvider(myCredentials) 153 | .build(); 154 | ``` 155 | 156 | The `region()` chosen doesn't matter when the endpoint is explicitly chosen 157 | with `endpointOverride()`, but nevertheless should be specified otherwise the 158 | SDK will try to look it up in a configuration file, and complain if it isn't 159 | set there. 160 | 161 | To use the Alternator load balancer and all Alternator nodes, all you need to 162 | change in the above code is to replace the `endpointOverride()` call by a call 163 | to `endpointProvider()`, giving it a new `AlternatorEndpointProvider` object 164 | which takes care of updating the knowledge of the live nodes in the Scylla 165 | cluster and choosing a different one for each request. 166 | 167 | The new code will look like this: 168 | 169 | ```java 170 | static AwsCredentialsProvider myCredentials = 171 | StaticCredentialsProvider.create(AwsBasicCredentials.create("myuser", "mypassword")); 172 | URI uri = URI.create("https://127.0.0.1:8043"); 173 | AlternatorEndpointProvider alternatorEndpointProvider = new AlternatorEndpointProvider(uri); 174 | DynamoDbClient client = DynamoDbClient.builder() 175 | .region(Region.US_EAST_1) 176 | .endpointProvider(alternatorEndpointProvider) 177 | .credentialsProvider(myCredentials) 178 | .build(); 179 | ``` 180 | 181 | Please note that the `endpointProvider()` API is new to AWS Java SDK 2.20 182 | (Release February 2023), so you should use this version or newer. 183 | 184 | The application can then use this `DynamoDBClient` object completely normally, 185 | just that each request will go to a different Alternator node, instead of all 186 | of them going to the same URL. 187 | 188 | The parameter `uri` is one known Alternator node, which is then contacted to 189 | discover the rest. After this initialization, this original node may go down 190 | at any time - any other already-known node can be used to retrieve the node 191 | list, and we no longer rely on the original node. 192 | 193 | You can see `src/test/java/com/scylladb/alternator/test/Demo2.java` for a 194 | complete example of using client-side load balancing with AWS SDK for Java v2. 195 | After building with `mvn package`, you can run this demo with the command: 196 | ``` 197 | mvn exec:java -Dexec.mainClass=com.scylladb.alternator.test.Demo2 -Dexec.classpathScope=test 198 | ``` 199 | 200 | #### Asyncronous operation in SDK v2 201 | 202 | When using SDK v2, you can achieve better scalability and performance using the asynchronous 203 | versions of API calls and `java.util.concurrent` completion chaining. 204 | To create a `DynamoDbAsyncClient` using alternator load balancing, the code should again just 205 | use the `endpointProvider()` method on the `DynamoDBAsyncClientBuilder`, passing an 206 | `AlternatorEndpointProvider` object. E.g., something like: 207 | 208 | ```java 209 | static AwsCredentialsProvider myCredentials = 210 | StaticCredentialsProvider.create(AwsBasicCredentials.create("myuser", "mypassword")); 211 | URI uri = URI.create("https://127.0.0.1:8043"); 212 | AlternatorEndpointProvider alternatorEndpointProvider = new AlternatorEndpointProvider(uri); 213 | DynamoDbAsyncClient client = DynamoDbAsyncClient.builder() 214 | .region(Region.US_EAST_1) 215 | .endpointProvider(alternatorEndpointProvider) 216 | .credentialsProvider(myCredentials) 217 | .build(); 218 | ``` 219 | 220 | You can see `src/test/java/com/scylladb/alternator/test/Demo3.java` for a 221 | complete example of using client-side load balancing with AWS SDK for Java v2 asynchronous API. 222 | After building with `mvn package`, you can run this demo with the command: 223 | ``` 224 | mvn exec:java -Dexec.mainClass=com.scylladb.alternator.test.Demo3 -Dexec.classpathScope=test 225 | ``` 226 | -------------------------------------------------------------------------------- /java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.scylladb.alternator 7 | load-balancing 8 | 1.0.4 9 | Scylla Alternator client performing load balancing 10 | DynamoDB client request handler balancing the load across all the nodes of a Scylla cluster 11 | https://github.com/scylladb/alternator-load-balancing 12 | 13 | 14 | UTF-8 15 | 1.8 16 | 1.8 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | com.amazonaws 25 | aws-java-sdk-bom 26 | 1.11.919 27 | pom 28 | import 29 | 30 | 31 | 32 | 33 | software.amazon.awssdk 34 | bom 35 | 2.25.31 36 | pom 37 | import 38 | 39 | 40 | 41 | 42 | 43 | 44 | com.amazonaws 45 | aws-java-sdk-dynamodb 46 | provided 47 | 48 | 49 | 50 | software.amazon.awssdk 51 | dynamodb 52 | provided 53 | 54 | 55 | software.amazon.awssdk 56 | apache-client 57 | 58 | 59 | net.sourceforge.argparse4j 60 | argparse4j 61 | 0.8.1 62 | test 63 | 64 | 65 | 66 | 67 | 68 | 69 | release 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-source-plugin 75 | 3.3.1 76 | 77 | 78 | attach-sources 79 | 80 | jar-no-fork 81 | 82 | 83 | 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-javadoc-plugin 88 | 3.6.3 89 | 90 | 91 | attach-javadocs 92 | 93 | jar 94 | 95 | 96 | 97 | 98 | 99 | org.apache.maven.plugins 100 | maven-gpg-plugin 101 | 3.2.4 102 | 103 | 104 | sign-artifacts 105 | verify 106 | 107 | sign 108 | 109 | 110 | 111 | 112 | 113 | org.sonatype.plugins 114 | nexus-staging-maven-plugin 115 | 1.6.13 116 | true 117 | 118 | ossrh 119 | https://oss.sonatype.org 120 | true 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | ossrh 131 | https://oss.sonatype.org/content/repositories/snapshots 132 | 133 | 134 | ossrh 135 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 136 | 137 | 138 | 139 | 140 | 141 | The Apache License, Version 2.0 142 | https://www.apache.org/licenses/LICENSE-2.0.txt 143 | 144 | 145 | 146 | scm:git:https://github.com/scylladb/alternator-load-balancing 147 | scm:git:https://github.com/scylladb/alternator-load-balancing 148 | https://github.com/scylladb/alternator-load-balancing 149 | HEAD 150 | 151 | 152 | 153 | Various 154 | ScyllaDB 155 | 156 | 157 | 158 | 159 | 160 | 161 | org.codehaus.mojo 162 | versions-maven-plugin 163 | 2.17.1 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /java/src/main/java/com/scylladb/alternator/AlternatorEndpointProvider.java: -------------------------------------------------------------------------------- 1 | package com.scylladb.alternator; 2 | 3 | import java.net.URI; 4 | import java.util.Map; 5 | import java.util.concurrent.CompletableFuture; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.logging.Logger; 8 | import software.amazon.awssdk.endpoints.Endpoint; 9 | import software.amazon.awssdk.services.dynamodb.endpoints.DynamoDbEndpointParams; 10 | import software.amazon.awssdk.services.dynamodb.endpoints.DynamoDbEndpointProvider; 11 | 12 | // AWS Java SDK v2 allows providing a DynamoDbEndpointProvider which can 13 | // choose a different endpoint for each request. Here we implement an 14 | // AlternatorEndpointProvider, which maintains up-to-date knowledge of the 15 | // live nodes in Alternator data center (by holding a AlternatorLiveNodes 16 | // object), and choose a different node for each request. 17 | /** 18 | * AlternatorEndpointProvider class. 19 | * 20 | * @author dmitry.kropachev 21 | */ 22 | public class AlternatorEndpointProvider implements DynamoDbEndpointProvider { 23 | private final AlternatorLiveNodes liveNodes; 24 | private final Map> futureCache; 25 | private static Logger logger = Logger.getLogger(AlternatorEndpointProvider.class.getName()); 26 | 27 | /** 28 | * Constructor for AlternatorEndpointProvider. 29 | * 30 | * @param seedURI a {@link java.net.URI} object 31 | */ 32 | public AlternatorEndpointProvider(URI seedURI) { 33 | this(seedURI, "", ""); 34 | } 35 | 36 | /** 37 | * Constructor for AlternatorEndpointProvider. 38 | * 39 | * @param seedURI a {@link java.net.URI} object 40 | * @param datacenter a {@link java.lang.String} object 41 | * @param rack a {@link java.lang.String} object 42 | * @since 1.0.1 43 | */ 44 | public AlternatorEndpointProvider(URI seedURI, String datacenter, String rack) { 45 | futureCache = new ConcurrentHashMap<>(); 46 | liveNodes = AlternatorLiveNodes.pickSupportedDatacenterRack(seedURI, datacenter, rack); 47 | liveNodes.start(); 48 | } 49 | 50 | /** {@inheritDoc} */ 51 | @Override 52 | public CompletableFuture resolveEndpoint(DynamoDbEndpointParams endpointParams) { 53 | URI uri = liveNodes.nextAsURI(); 54 | CompletableFuture endpoint = futureCache.getOrDefault(uri, null); 55 | if (endpoint != null) { 56 | return endpoint; 57 | } 58 | endpoint = new CompletableFuture<>(); 59 | endpoint.complete(Endpoint.builder().url(uri).build()); 60 | futureCache.put(uri, endpoint); 61 | return endpoint; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /java/src/main/java/com/scylladb/alternator/AlternatorRequestHandler.java: -------------------------------------------------------------------------------- 1 | package com.scylladb.alternator; 2 | 3 | import com.amazonaws.Request; 4 | import com.amazonaws.handlers.RequestHandler2; 5 | import java.net.URI; 6 | import java.util.logging.Logger; 7 | 8 | /* AlternatorRequestHandler is RequestHandler2 implementation for AWS SDK 9 | * for Java v1. It tells the SDK to replace the endpoint in the request, 10 | * whatever it was, with the next Alternator node. 11 | */ 12 | /** 13 | * AlternatorRequestHandler class. 14 | * 15 | * @author dmitry.kropachev 16 | */ 17 | public class AlternatorRequestHandler extends RequestHandler2 { 18 | 19 | private static Logger logger = Logger.getLogger(AlternatorRequestHandler.class.getName()); 20 | 21 | AlternatorLiveNodes liveNodes; 22 | 23 | /** 24 | * Constructor for AlternatorRequestHandler. 25 | * 26 | * @param seedURI a {@link java.net.URI} object 27 | */ 28 | public AlternatorRequestHandler(URI seedURI) { 29 | this(seedURI, "", ""); 30 | } 31 | 32 | /** 33 | * Constructor for AlternatorRequestHandler. 34 | * 35 | * @param seedURI a {@link java.net.URI} object 36 | * @param datacenter a {@link java.lang.String} object 37 | * @param rack a {@link java.lang.String} object 38 | * @since 1.0.1 39 | */ 40 | public AlternatorRequestHandler(URI seedURI, String datacenter, String rack) { 41 | liveNodes = AlternatorLiveNodes.pickSupportedDatacenterRack(seedURI, datacenter, rack); 42 | liveNodes.start(); 43 | } 44 | 45 | /** {@inheritDoc} */ 46 | @Override 47 | public void beforeRequest(Request request) { 48 | request.setEndpoint(liveNodes.nextAsURI()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /java/src/test/java/com/scylladb/alternator/test/Demo1.java: -------------------------------------------------------------------------------- 1 | package com.scylladb.alternator.test; 2 | 3 | import com.amazonaws.SDKGlobalConfiguration; 4 | import com.amazonaws.auth.AWSCredentialsProvider; 5 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 6 | import com.amazonaws.auth.BasicAWSCredentials; 7 | import com.amazonaws.client.builder.AwsClientBuilder; 8 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; 9 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; 10 | import com.amazonaws.services.dynamodbv2.document.DynamoDB; 11 | import com.amazonaws.services.dynamodbv2.document.Table; 12 | import com.amazonaws.services.dynamodbv2.document.TableCollection; 13 | import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; 14 | import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; 15 | import com.amazonaws.services.dynamodbv2.model.KeyType; 16 | import com.amazonaws.services.dynamodbv2.model.ListTablesResult; 17 | import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; 18 | import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; 19 | import com.scylladb.alternator.AlternatorRequestHandler; 20 | import java.net.URI; 21 | import java.net.URISyntaxException; 22 | import java.security.cert.X509Certificate; 23 | import java.util.Arrays; 24 | import java.util.Random; 25 | import java.util.logging.ConsoleHandler; 26 | import java.util.logging.Level; 27 | import java.util.logging.Logger; 28 | import javax.net.ssl.HostnameVerifier; 29 | import javax.net.ssl.HttpsURLConnection; 30 | import javax.net.ssl.SSLContext; 31 | import javax.net.ssl.SSLSession; 32 | import javax.net.ssl.TrustManager; 33 | import javax.net.ssl.X509TrustManager; 34 | import net.sourceforge.argparse4j.ArgumentParsers; 35 | import net.sourceforge.argparse4j.inf.ArgumentParser; 36 | import net.sourceforge.argparse4j.inf.ArgumentParserException; 37 | import net.sourceforge.argparse4j.inf.Namespace; 38 | 39 | public class Demo1 { 40 | 41 | // The following is the "traditional" way to get a DynamoDB connection to 42 | // a specific endpoint URL, with no client-side load balancing, or any 43 | // Alternator-specific code. 44 | static DynamoDB getTraditionalClient(URI url, AWSCredentialsProvider myCredentials) { 45 | AmazonDynamoDB client = 46 | AmazonDynamoDBClientBuilder.standard() 47 | .withEndpointConfiguration( 48 | new AwsClientBuilder.EndpointConfiguration(url.toString(), "region-doesnt-matter")) 49 | .withCredentials(myCredentials) 50 | .build(); 51 | return new DynamoDB(client); 52 | } 53 | 54 | // And this is the Alternator-specific way to get a DynamoDB connection 55 | // which load-balances several Scylla nodes. 56 | static DynamoDB getAlternatorClient( 57 | URI uri, AWSCredentialsProvider myCredentials, String datacenter, String rack) { 58 | AlternatorRequestHandler handler = new AlternatorRequestHandler(uri, datacenter, rack); 59 | AmazonDynamoDB client = 60 | AmazonDynamoDBClientBuilder.standard() 61 | // The endpoint doesn't matter, we will override it anyway in the 62 | // RequestHandler, but without setting it the library will complain 63 | // if "region" isn't set in the configuration file. 64 | .withRegion("region-doesnt-matter") 65 | .withRequestHandlers(handler) 66 | .withCredentials(myCredentials) 67 | .build(); 68 | return new DynamoDB(client); 69 | } 70 | 71 | public static void main(String[] args) { 72 | // The load balancer library logs the list of live nodes, and hosts 73 | // it chooses to send requests to, if the FINE logging level is 74 | // enabled. 75 | Logger logger = Logger.getLogger("com.scylladb.alternator"); 76 | ConsoleHandler handler = new ConsoleHandler(); 77 | handler.setLevel(Level.FINEST); 78 | logger.setLevel(Level.FINEST); 79 | logger.addHandler(handler); 80 | logger.setUseParentHandlers(false); 81 | 82 | ArgumentParser parser = 83 | ArgumentParsers.newFor("Demo1") 84 | .build() 85 | .defaultHelp(true) 86 | .description("Simple example of AWS SDK v1 alternator access"); 87 | 88 | try { 89 | parser 90 | .addArgument("-e", "--endpoint") 91 | .setDefault(new URI("http://localhost:8043")) 92 | .help("DynamoDB/Alternator endpoint"); 93 | } catch (URISyntaxException e) { 94 | throw new RuntimeException(e); 95 | } 96 | parser.addArgument("-u", "--user").setDefault("none").help("Credentials username"); 97 | parser.addArgument("-p", "--password").setDefault("none").help("Credentials password"); 98 | parser 99 | .addArgument("--datacenter") 100 | .type(String.class) 101 | .setDefault("") 102 | .help( 103 | "Target only nodes from particular datacenter. If it is not provided it is going to target datacenter of the endpoint."); 104 | parser 105 | .addArgument("--rack") 106 | .type(String.class) 107 | .setDefault("") 108 | .help("Target only nodes from particular rack"); 109 | parser 110 | .addArgument("--no-lb") 111 | .type(Boolean.class) 112 | .setDefault(false) 113 | .help("Turn off load balancing"); 114 | 115 | Namespace ns = null; 116 | try { 117 | ns = parser.parseArgs(args); 118 | } catch (ArgumentParserException e) { 119 | parser.handleError(e); 120 | System.exit(1); 121 | } 122 | 123 | String endpoint = ns.getString("endpoint"); 124 | String user = ns.getString("user"); 125 | String pass = ns.getString("password"); 126 | String datacenter = ns.getString("datacenter"); 127 | String rack = ns.getString("rack"); 128 | Boolean disableLoadBalancing = ns.getBoolean("no-lb"); 129 | 130 | // In our test setup, the Alternator HTTPS server set up with a self- 131 | // signed certficate, so we need to disable certificate checking. 132 | // Obviously, this doesn't need to be done in production code. 133 | disableCertificateChecks(); 134 | 135 | AWSCredentialsProvider myCredentials = 136 | new AWSStaticCredentialsProvider(new BasicAWSCredentials(user, pass)); 137 | DynamoDB ddb; 138 | if (disableLoadBalancing == null || !disableLoadBalancing) { 139 | ddb = getAlternatorClient(URI.create(endpoint), myCredentials, datacenter, rack); 140 | } else { 141 | ddb = getTraditionalClient(URI.create(endpoint), myCredentials); 142 | } 143 | 144 | Random rand = new Random(); 145 | String tabName = "table" + rand.nextInt(1000000); 146 | Table tab = 147 | ddb.createTable( 148 | tabName, 149 | Arrays.asList( 150 | new KeySchemaElement("k", KeyType.HASH), new KeySchemaElement("c", KeyType.RANGE)), 151 | Arrays.asList( 152 | new AttributeDefinition("k", ScalarAttributeType.N), 153 | new AttributeDefinition("c", ScalarAttributeType.N)), 154 | new ProvisionedThroughput(0L, 0L)); 155 | // run ListTables several times 156 | for (int i = 0; i < 10; i++) { 157 | TableCollection tables = ddb.listTables(); 158 | System.out.println(tables.firstPage()); 159 | } 160 | tab.delete(); 161 | ddb.shutdown(); 162 | } 163 | 164 | // A hack to disable SSL certificate checks. Useful when running with 165 | // a self-signed certificate. Shouldn't be used in production of course 166 | static void disableCertificateChecks() { 167 | // This is used in AWS SDK v1: 168 | System.setProperty(SDKGlobalConfiguration.DISABLE_CERT_CHECKING_SYSTEM_PROPERTY, "true"); 169 | 170 | // And this is used by Java's HttpsURLConnection (which we use only 171 | // in AlternatorLiveNodes): 172 | TrustManager[] trustAllCerts = 173 | new TrustManager[] { 174 | new X509TrustManager() { 175 | public java.security.cert.X509Certificate[] getAcceptedIssuers() { 176 | return null; 177 | } 178 | 179 | public void checkClientTrusted(X509Certificate[] certs, String authType) {} 180 | 181 | public void checkServerTrusted(X509Certificate[] certs, String authType) {} 182 | } 183 | }; 184 | try { 185 | SSLContext sc = SSLContext.getInstance("SSL"); 186 | sc.init(null, trustAllCerts, new java.security.SecureRandom()); 187 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); 188 | } catch (Exception e) { 189 | e.printStackTrace(); 190 | } 191 | HttpsURLConnection.setDefaultHostnameVerifier( 192 | new HostnameVerifier() { 193 | @Override 194 | public boolean verify(String arg0, SSLSession arg1) { 195 | return true; 196 | } 197 | }); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /java/src/test/java/com/scylladb/alternator/test/Demo2.java: -------------------------------------------------------------------------------- 1 | package com.scylladb.alternator.test; 2 | 3 | import com.scylladb.alternator.AlternatorEndpointProvider; 4 | import java.net.URI; 5 | import java.net.URISyntaxException; 6 | import java.security.cert.X509Certificate; 7 | import java.util.logging.ConsoleHandler; 8 | import java.util.logging.Level; 9 | import java.util.logging.Logger; 10 | import javax.net.ssl.HostnameVerifier; 11 | import javax.net.ssl.HttpsURLConnection; 12 | import javax.net.ssl.SSLContext; 13 | import javax.net.ssl.SSLSession; 14 | import javax.net.ssl.TrustManager; 15 | import javax.net.ssl.X509TrustManager; 16 | import net.sourceforge.argparse4j.ArgumentParsers; 17 | import net.sourceforge.argparse4j.inf.ArgumentParser; 18 | import net.sourceforge.argparse4j.inf.ArgumentParserException; 19 | import net.sourceforge.argparse4j.inf.Namespace; 20 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 21 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 22 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 23 | import software.amazon.awssdk.http.SdkHttpClient; 24 | import software.amazon.awssdk.http.SdkHttpConfigurationOption; 25 | import software.amazon.awssdk.http.apache.ApacheHttpClient; 26 | import software.amazon.awssdk.regions.Region; 27 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 28 | import software.amazon.awssdk.services.dynamodb.model.DescribeEndpointsRequest; 29 | import software.amazon.awssdk.services.dynamodb.model.DescribeEndpointsResponse; 30 | import software.amazon.awssdk.utils.AttributeMap; 31 | 32 | public class Demo2 { 33 | // Set here the authentication credentials needed by the server: 34 | 35 | // The following is the "traditional" way to get a DynamoDB connection to 36 | // a specific endpoint URL, with no client-side load balancing, or any 37 | // Alternator-specific code. 38 | static DynamoDbClient getTraditionalClient(URI url, AwsCredentialsProvider myCredentials) { 39 | // To support HTTPS connections to a test server *without* checking 40 | // SSL certificates we need the httpClient() hack. It's of course not 41 | // needed in a production installation. 42 | SdkHttpClient http = 43 | ApacheHttpClient.builder() 44 | .buildWithDefaults( 45 | AttributeMap.builder() 46 | .put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true) 47 | .build()); 48 | return DynamoDbClient.builder() 49 | .endpointOverride(url) 50 | .credentialsProvider(myCredentials) 51 | .httpClient(http) 52 | .build(); 53 | } 54 | 55 | // And this is the Alternator-specific way to get a DynamoDB connection 56 | // which load-balances several Scylla nodes. 57 | // Basically the only change is replacing the endpointOverride() call 58 | // with its fixed endpoind URL, with an endpointProvider() call, giving 59 | // an AlternatorEndpointProvider object. 60 | static DynamoDbClient getAlternatorClient( 61 | URI url, AwsCredentialsProvider myCredentials, String datacenter, String rack) { 62 | // To support HTTPS connections to a test server *without* checking 63 | // SSL certificates we need the httpClient() hack. It's of course not 64 | // needed in a production installation. 65 | SdkHttpClient http = 66 | ApacheHttpClient.builder() 67 | .buildWithDefaults( 68 | AttributeMap.builder() 69 | .put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true) 70 | .build()); 71 | AlternatorEndpointProvider alternatorEndpointProvider = 72 | new AlternatorEndpointProvider(url, datacenter, rack); 73 | return DynamoDbClient.builder() 74 | .credentialsProvider(myCredentials) 75 | .httpClient(http) 76 | .region(Region.US_EAST_1) // unused, but if missing can result in error 77 | .endpointProvider(alternatorEndpointProvider) 78 | .build(); 79 | } 80 | 81 | public static void main(String[] args) { 82 | // The load balancer library logs the list of live nodes, and hosts 83 | // it chooses to send requests to, if the FINE logging level is 84 | // enabled. 85 | Logger logger = Logger.getLogger("com.scylladb.alternator"); 86 | ConsoleHandler handler = new ConsoleHandler(); 87 | handler.setLevel(Level.FINEST); 88 | logger.setLevel(Level.FINEST); 89 | logger.addHandler(handler); 90 | logger.setUseParentHandlers(false); 91 | 92 | ArgumentParser parser = 93 | ArgumentParsers.newFor("Demo2") 94 | .build() 95 | .defaultHelp(true) 96 | .description("Simple example of AWS SDK v1 alternator access"); 97 | 98 | try { 99 | parser 100 | .addArgument("-e", "--endpoint") 101 | .setDefault(new URI("http://localhost:8043")) 102 | .help("DynamoDB/Alternator endpoint"); 103 | } catch (URISyntaxException e) { 104 | throw new RuntimeException(e); 105 | } 106 | parser.addArgument("-u", "--user").setDefault("none").help("Credentials username"); 107 | parser.addArgument("-p", "--password").setDefault("none").help("Credentials password"); 108 | parser 109 | .addArgument("--datacenter") 110 | .type(String.class) 111 | .setDefault("") 112 | .help( 113 | "Target only nodes from particular datacenter. If it is not provided it is going to target datacenter of the endpoint."); 114 | parser 115 | .addArgument("--rack") 116 | .type(String.class) 117 | .setDefault("") 118 | .help("Target only nodes from particular rack"); 119 | parser 120 | .addArgument("--no-lb") 121 | .type(Boolean.class) 122 | .setDefault(false) 123 | .help("Turn off load balancing"); 124 | 125 | Namespace ns = null; 126 | try { 127 | ns = parser.parseArgs(args); 128 | } catch (ArgumentParserException e) { 129 | parser.handleError(e); 130 | System.exit(1); 131 | } 132 | 133 | String endpoint = ns.getString("endpoint"); 134 | String user = ns.getString("user"); 135 | String pass = ns.getString("password"); 136 | String datacenter = ns.getString("datacenter"); 137 | String rack = ns.getString("rack"); 138 | Boolean disableLoadBalancing = ns.getBoolean("no-lb"); 139 | 140 | // In our test setup, the Alternator HTTPS server set up with a self- 141 | // signed certificate, so we need to disable certificate checking. 142 | // Obviously, this doesn't need to be done in production code. 143 | disableCertificateChecks(); 144 | AwsCredentialsProvider myCredentials = 145 | StaticCredentialsProvider.create(AwsBasicCredentials.create(user, pass)); 146 | DynamoDbClient ddb; 147 | if (disableLoadBalancing == null || !disableLoadBalancing) { 148 | ddb = getAlternatorClient(URI.create(endpoint), myCredentials, datacenter, rack); 149 | } else { 150 | ddb = getTraditionalClient(URI.create(endpoint), myCredentials); 151 | } 152 | 153 | // run DescribeEndpoints several times 154 | for (int i = 0; i < 10; i++) { 155 | DescribeEndpointsRequest request = DescribeEndpointsRequest.builder().build(); 156 | DescribeEndpointsResponse response = ddb.describeEndpoints(request); 157 | System.out.println(response); 158 | } 159 | // FIXME: The AWS SDK leaves behind an IdleConnectionReaper thread, 160 | // which causes mvn to hang waiting for it to shut down (which it 161 | // won't). Perhaps if we hold the HttpClient object and close() it, 162 | // it will get rid of this reaper object. 163 | // https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/core/client/builder/SdkSyncClientBuilder.html#httpClient-software.amazon.awssdk.http.SdkHttpClient- 164 | // explains that with httpClient(), "This client must be closed by the 165 | // user when it is ready to be disposed. The SDK will not close the 166 | // HTTP client when the service client is closed." Maybe we should 167 | // use httpClientBuilder() instead, which doesn't have this problem? 168 | } 169 | 170 | // A hack to disable SSL certificate checks. Useful when running with 171 | // a self-signed certificate. Shouldn't be used in production of course 172 | static void disableCertificateChecks() { 173 | // Unfortunately, the following is no longer supported by AWS SDK v2 174 | // as it was in v1, so we needed to add the option when building the 175 | // HTTP client. 176 | // System.setProperty(SDKGlobalConfiguration.DISABLE_CERT_CHECKING_SYSTEM_PROPERTY, "true"); 177 | 178 | // And this is used by Java's HttpsURLConnection (which we use only 179 | // in AlternatorLiveNodes): 180 | TrustManager[] trustAllCerts = 181 | new TrustManager[] { 182 | new X509TrustManager() { 183 | public java.security.cert.X509Certificate[] getAcceptedIssuers() { 184 | return null; 185 | } 186 | 187 | public void checkClientTrusted(X509Certificate[] certs, String authType) {} 188 | 189 | public void checkServerTrusted(X509Certificate[] certs, String authType) {} 190 | } 191 | }; 192 | try { 193 | SSLContext sc = SSLContext.getInstance("SSL"); 194 | sc.init(null, trustAllCerts, new java.security.SecureRandom()); 195 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); 196 | } catch (Exception e) { 197 | e.printStackTrace(); 198 | } 199 | HttpsURLConnection.setDefaultHostnameVerifier( 200 | new HostnameVerifier() { 201 | @Override 202 | public boolean verify(String arg0, SSLSession arg1) { 203 | return true; 204 | } 205 | }); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /java/src/test/java/com/scylladb/alternator/test/Demo3.java: -------------------------------------------------------------------------------- 1 | package com.scylladb.alternator.test; 2 | 3 | import static java.util.concurrent.Executors.newFixedThreadPool; 4 | 5 | import com.scylladb.alternator.AlternatorEndpointProvider; 6 | import java.net.MalformedURLException; 7 | import java.net.URI; 8 | import java.net.URL; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.concurrent.CompletableFuture; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.logging.ConsoleHandler; 14 | import java.util.logging.Level; 15 | // For enabling trace-level logging 16 | import java.util.logging.Logger; 17 | import net.sourceforge.argparse4j.ArgumentParsers; 18 | import net.sourceforge.argparse4j.inf.ArgumentParser; 19 | import net.sourceforge.argparse4j.inf.ArgumentParserException; 20 | import net.sourceforge.argparse4j.inf.Namespace; 21 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 22 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 23 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 24 | import software.amazon.awssdk.core.client.config.ClientAsyncConfiguration; 25 | import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption; 26 | import software.amazon.awssdk.core.internal.http.loader.DefaultSdkAsyncHttpClientBuilder; 27 | import software.amazon.awssdk.http.SdkHttpConfigurationOption; 28 | import software.amazon.awssdk.http.async.SdkAsyncHttpClient; 29 | import software.amazon.awssdk.regions.Region; 30 | import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; 31 | import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClientBuilder; 32 | import software.amazon.awssdk.services.dynamodb.model.DescribeEndpointsRequest; 33 | import software.amazon.awssdk.utils.AttributeMap; 34 | 35 | public class Demo3 { 36 | public static void main(String[] args) throws MalformedURLException { 37 | ArgumentParser parser = 38 | ArgumentParsers.newFor("Demo3") 39 | .build() 40 | .defaultHelp(true) 41 | .description("Simple example of AWS SDK v2 async alternator access"); 42 | 43 | parser 44 | .addArgument("-e", "--endpoint") 45 | .setDefault(new URL("http://localhost:8043")) 46 | .help("DynamoDB/Alternator endpoint"); 47 | 48 | parser.addArgument("-u", "--user").setDefault("none").help("Credentials username"); 49 | parser.addArgument("-p", "--password").setDefault("none").help("Credentials password"); 50 | parser.addArgument("-r", "--region").setDefault("us-east-1").help("AWS region"); 51 | parser 52 | .addArgument("--threads") 53 | .type(Integer.class) 54 | .setDefault(Runtime.getRuntime().availableProcessors() * 2) 55 | .help("Max worker threads"); 56 | parser 57 | .addArgument("--trust-ssl") 58 | .type(Boolean.class) 59 | .setDefault(false) 60 | .help("Trust all certificates"); 61 | parser 62 | .addArgument("--datacenter") 63 | .type(String.class) 64 | .setDefault("") 65 | .help( 66 | "Target only nodes from particular datacenter. If it is not provided it is going to target datacenter of the endpoint."); 67 | parser 68 | .addArgument("--rack") 69 | .type(String.class) 70 | .setDefault("") 71 | .help("Target only nodes from particular rack"); 72 | 73 | Namespace ns = null; 74 | try { 75 | ns = parser.parseArgs(args); 76 | } catch (ArgumentParserException e) { 77 | parser.handleError(e); 78 | System.exit(1); 79 | } 80 | 81 | String endpoint = ns.getString("endpoint"); 82 | String user = ns.getString("user"); 83 | String pass = ns.getString("password"); 84 | int threads = ns.getInt("threads"); 85 | Region region = Region.of(ns.getString("region")); 86 | Boolean trustSSL = ns.getBoolean("trust-ssl"); 87 | String datacenter = ns.getString("datacenter"); 88 | String rack = ns.getString("rack"); 89 | 90 | // The load balancer library logs the list of live nodes, and hosts 91 | // it chooses to send requests to, if the FINE logging level is 92 | // enabled. 93 | Logger logger = Logger.getLogger("com.scylladb.alternator"); 94 | ConsoleHandler handler = new ConsoleHandler(); 95 | handler.setLevel(Level.FINEST); 96 | logger.setLevel(Level.FINEST); 97 | logger.addHandler(handler); 98 | logger.setUseParentHandlers(false); 99 | 100 | DynamoDbAsyncClientBuilder b = DynamoDbAsyncClient.builder().region(region); 101 | ExecutorService executor = newFixedThreadPool(threads); 102 | ClientAsyncConfiguration cas = 103 | ClientAsyncConfiguration.builder() 104 | .advancedOption(SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR, executor) 105 | .build(); 106 | b.asyncConfiguration(cas); 107 | 108 | if (endpoint != null) { 109 | URI uri = URI.create(endpoint); 110 | AlternatorEndpointProvider alternatorEndpointProvider = 111 | new AlternatorEndpointProvider(uri, datacenter, rack); 112 | 113 | if (trustSSL != null && trustSSL.booleanValue()) { 114 | // In our test setup, the Alternator HTTPS server set up with a 115 | // self-signed certficate, so we need to disable certificate 116 | // checking. Obviously, this doesn't need to be done in 117 | // production code. 118 | SdkAsyncHttpClient http = 119 | new DefaultSdkAsyncHttpClientBuilder() 120 | .buildWithDefaults( 121 | AttributeMap.builder() 122 | .put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true) 123 | .build()); 124 | b.httpClient(http); 125 | } 126 | b.endpointProvider(alternatorEndpointProvider); 127 | } 128 | 129 | if (user != null) { 130 | AwsCredentialsProvider cp = 131 | StaticCredentialsProvider.create(AwsBasicCredentials.create(user, pass)); 132 | b.credentialsProvider(cp); 133 | } 134 | 135 | DynamoDbAsyncClient dynamoDBClient = b.build(); 136 | 137 | // run DescribeEndpoints several times 138 | 139 | List> responses = new ArrayList<>(); 140 | 141 | for (int i = 0; i < 10; i++) { 142 | responses.add( 143 | dynamoDBClient 144 | .describeEndpoints(DescribeEndpointsRequest.builder().build()) 145 | .thenAccept(response -> System.out.println(response))); 146 | } 147 | 148 | CompletableFuture.allOf(responses.toArray(new CompletableFuture[responses.size()])).join(); 149 | 150 | System.out.println("Done"); 151 | System.exit(0); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /javascript/Alternator.js: -------------------------------------------------------------------------------- 1 | // Example usage: 2 | // AWS = require('aws-sdk'); 3 | // alternator = require('./Alternator'); 4 | // alternator.init(AWS, "http", 8000, ["127.0.0.1"]); 5 | // (...) 6 | // alternator.done(); 7 | 8 | var AWS = require("aws-sdk"); 9 | var http = require("http"); 10 | var https = require("https"); 11 | var dns = require("dns"); 12 | 13 | const FAKE_HOST = "dog.scylladb.com"; 14 | exports.FAKE_HOST = FAKE_HOST; 15 | 16 | var protocol; 17 | var hostIdx = 0; 18 | var hosts; 19 | var port; 20 | var done = false; 21 | var updatePromise; 22 | 23 | var agent = new http.Agent; 24 | 25 | var oldCreateConnection = agent.createConnection; 26 | agent.createConnection = function(options, callback = null) { 27 | options.lookup = function(hostname, options = null, callback) { 28 | if (hostname == FAKE_HOST) { 29 | var host = hosts[hostIdx]; 30 | hostIdx = (hostIdx + 1) % hosts.length; 31 | return dns.lookup(host, options, callback); 32 | } 33 | return dns.lookup(hostname, options, callback); 34 | }; 35 | return oldCreateConnection(options, callback); 36 | }; 37 | 38 | exports.agent = agent; 39 | 40 | async function updateHosts() { 41 | if (done) { 42 | return; 43 | } 44 | let proto = (protocol == "https") ? https : http; 45 | proto.get(protocol + "://" + hosts[hostIdx] + ":" + 8000 + "/localnodes", (resp) => { 46 | resp.on('data', (payload) => { 47 | payload = JSON.parse(payload); 48 | hosts = payload; 49 | }); 50 | }); 51 | await new Promise(r => setTimeout(r, 1000)); 52 | return updateHosts(); 53 | } 54 | 55 | exports.init = function(AWS, initialProtocol, initialPort, initialHosts) { 56 | protocol = initialProtocol; 57 | hosts = initialHosts; 58 | port = initialPort; 59 | AWS.config.update({ 60 | region: "world", 61 | endpoint: protocol + "://" + FAKE_HOST + ":" + initialPort, 62 | httpOptions:{ 63 | agent: agent 64 | } 65 | }); 66 | done = false; 67 | updatePromise = updateHosts().catch((error) => { 68 | console.error(error); 69 | });; 70 | } 71 | 72 | exports.done = function() { 73 | done = true; 74 | } 75 | -------------------------------------------------------------------------------- /javascript/MoviesCreateTable.js: -------------------------------------------------------------------------------- 1 | // snippet-sourcedescription:[ ] 2 | // snippet-service:[dynamodb] 3 | // snippet-keyword:[JavaScript] 4 | // snippet-sourcesyntax:[javascript] 5 | // snippet-keyword:[Amazon DynamoDB] 6 | // snippet-keyword:[Code Sample] 7 | // snippet-keyword:[ ] 8 | // snippet-sourcetype:[full-example] 9 | // snippet-sourcedate:[ ] 10 | // snippet-sourceauthor:[AWS] 11 | // snippet-start:[dynamodb.JavaScript.CodeExample.MoviesCreateTable] 12 | 13 | /** 14 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 15 | * 16 | * This file is licensed under the Apache License, Version 2.0 (the "License"). 17 | * You may not use this file except in compliance with the License. A copy of 18 | * the License is located at 19 | * 20 | * http://aws.amazon.com/apache2.0/ 21 | * 22 | * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 23 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the 24 | * specific language governing permissions and limitations under the License. 25 | */ 26 | var AWS = require("aws-sdk"); 27 | 28 | AWS.config.update({ 29 | region: "us-west-2", 30 | endpoint: "http://localhost:8000" 31 | }); 32 | 33 | var dynamodb = new AWS.DynamoDB(); 34 | 35 | var params = { 36 | TableName : "Movies", 37 | KeySchema: [ 38 | { AttributeName: "year", KeyType: "HASH"}, //Partition key 39 | { AttributeName: "title", KeyType: "RANGE" } //Sort key 40 | ], 41 | AttributeDefinitions: [ 42 | { AttributeName: "year", AttributeType: "N" }, 43 | { AttributeName: "title", AttributeType: "S" } 44 | ], 45 | ProvisionedThroughput: { 46 | ReadCapacityUnits: 10, 47 | WriteCapacityUnits: 10 48 | } 49 | }; 50 | 51 | dynamodb.createTable(params, function(err, data) { 52 | if (err) { 53 | console.error("Unable to create table. Error JSON:", JSON.stringify(err, null, 2)); 54 | } else { 55 | console.log("Created table. Table description JSON:", JSON.stringify(data, null, 2)); 56 | } 57 | }); 58 | // snippet-end:[dynamodb.JavaScript.CodeExample.MoviesCreateTable] -------------------------------------------------------------------------------- /javascript/MoviesItemOps01.js: -------------------------------------------------------------------------------- 1 | // snippet-sourcedescription:[ ] 2 | // snippet-service:[dynamodb] 3 | // snippet-keyword:[JavaScript] 4 | // snippet-sourcesyntax:[javascript] 5 | // snippet-keyword:[Amazon DynamoDB] 6 | // snippet-keyword:[Code Sample] 7 | // snippet-keyword:[ ] 8 | // snippet-sourcetype:[full-example] 9 | // snippet-sourcedate:[ ] 10 | // snippet-sourceauthor:[AWS] 11 | // snippet-start:[dynamodb.JavaScript.CodeExample.MoviesItemOps01] 12 | 13 | /** 14 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 15 | * 16 | * This file is licensed under the Apache License, Version 2.0 (the "License"). 17 | * You may not use this file except in compliance with the License. A copy of 18 | * the License is located at 19 | * 20 | * http://aws.amazon.com/apache2.0/ 21 | * 22 | * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 23 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the 24 | * specific language governing permissions and limitations under the License. 25 | */ 26 | var AWS = require("aws-sdk"); 27 | 28 | AWS.config.update({ 29 | region: "us-west-2", 30 | endpoint: "http://localhost:8000" 31 | }); 32 | 33 | var docClient = new AWS.DynamoDB.DocumentClient(); 34 | 35 | var table = "Movies"; 36 | 37 | var year = 2015; 38 | var title = "The Big New Movie"; 39 | 40 | var params = { 41 | TableName:table, 42 | Item:{ 43 | "year": year, 44 | "title": title, 45 | "info":{ 46 | "plot": "Nothing happens at all.", 47 | "rating": 0 48 | } 49 | } 50 | }; 51 | 52 | console.log("Adding a new item..."); 53 | docClient.put(params, function(err, data) { 54 | if (err) { 55 | console.error("Unable to add item. Error JSON:", JSON.stringify(err, null, 2)); 56 | } else { 57 | console.log("Added item:", JSON.stringify(data, null, 2)); 58 | } 59 | }); 60 | // snippet-end:[dynamodb.JavaScript.CodeExample.MoviesItemOps01] -------------------------------------------------------------------------------- /javascript/MoviesItemOps02.js: -------------------------------------------------------------------------------- 1 | // snippet-sourcedescription:[ ] 2 | // snippet-service:[dynamodb] 3 | // snippet-keyword:[JavaScript] 4 | // snippet-sourcesyntax:[javascript] 5 | // snippet-keyword:[Amazon DynamoDB] 6 | // snippet-keyword:[Code Sample] 7 | // snippet-keyword:[ ] 8 | // snippet-sourcetype:[full-example] 9 | // snippet-sourcedate:[ ] 10 | // snippet-sourceauthor:[AWS] 11 | // snippet-start:[dynamodb.JavaScript.CodeExample.MoviesItemOps02] 12 | 13 | /** 14 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 15 | * 16 | * This file is licensed under the Apache License, Version 2.0 (the "License"). 17 | * You may not use this file except in compliance with the License. A copy of 18 | * the License is located at 19 | * 20 | * http://aws.amazon.com/apache2.0/ 21 | * 22 | * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 23 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the 24 | * specific language governing permissions and limitations under the License. 25 | */ 26 | var AWS = require("aws-sdk"); 27 | var alternator = require("./Alternator"); 28 | 29 | alternator.init(AWS, "http", 8000, ["127.0.0.1"]); 30 | 31 | var docClient = new AWS.DynamoDB.DocumentClient(); 32 | 33 | var table = "Movies"; 34 | 35 | var year = 2015; 36 | var title = "The Big New Movie"; 37 | 38 | var params = { 39 | TableName: table, 40 | Key:{ 41 | "year": year, 42 | "title": title 43 | } 44 | }; 45 | 46 | docClient.get(params, function(err, data) { 47 | if (err) { 48 | console.error("Unable to read item. Error JSON:", JSON.stringify(err, null, 2)); 49 | } else { 50 | console.log("GetItem succeeded:", JSON.stringify(data, null, 2)); 51 | } 52 | }); 53 | docClient.get(params, function(err, data) { 54 | if (err) { 55 | console.error("Unable to read item. Error JSON:", JSON.stringify(err, null, 2)); 56 | } else { 57 | console.log("GetItem succeeded:", JSON.stringify(data, null, 2)); 58 | } 59 | }); 60 | 61 | alternator.done(); 62 | 63 | // snippet-end:[dynamodb.JavaScript.CodeExample.MoviesItemOps02] 64 | -------------------------------------------------------------------------------- /javascript/README.md: -------------------------------------------------------------------------------- 1 | # Alternator Load Balancing: javascript 2 | 3 | This directory contains a load-balancing wrapper for the javascript driver. 4 | 5 | ## Contents 6 | 7 | Alternator load balancing wrapper consists of a single `Alternator.js` module. By using this module's initialization routines it's possible to set the driver up for client-side load balancing. 8 | 9 | ## Usage 10 | 11 | In order to switch from the stock driver to Alternator load balancing, it's enough to replace the initialization code in the existing application: 12 | ```javascript 13 | AWS.config.update({ 14 | region: "us-west-2" 15 | }); 16 | ``` 17 | to 18 | ```javascript 19 | alternator.init(AWS, "http", 8000, ["127.0.0.1"]); 20 | ``` 21 | After that all following requests will be implicitly routed to Alternator nodes. 22 | Parameters accepted by the Alternator client are: 23 | 1. `protocol`: `"http"` or `"https"`, used for client-server communication. 24 | 2. `port`: Port of the Alternator nodes. 25 | The code assumes that all Alternator nodes share an identical port for client communication. 26 | 3. `hosts`: list of hostnames of the Alternator nodes. 27 | Periodically, one of the nodes will be contacted to retrieve the cluster topology information. 28 | 29 | It's enough to provide a single Alternator node to the initialization function, as the list of active nodes is periodically refreshed in the background. 30 | 31 | ## Details 32 | 33 | Alternator load balancing for javascript works by overriding internal methods of the stock javascript driver, which causes the requests to different Alternator nodes. Initially, the driver contacts one of the Alternator nodes and retrieves the list of active nodes which can be use to accept user requests. This list is perodically refreshed in order to ensure that any topology changes are taken into account. Once a client sends a request, the load balancing layer picks one of the active Alternator nodes as the target. Currently, nodes are picked in a round-robin fashion. 34 | 35 | -------------------------------------------------------------------------------- /javascript/run_example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | node MoviesCreateTable.js 4 | node MoviesItemOps01.js 5 | node MoviesItemOps02.js 6 | -------------------------------------------------------------------------------- /python/Makefile: -------------------------------------------------------------------------------- 1 | define dl_tgz 2 | @if ! $(1) 2>/dev/null 1>&2; then \ 3 | [ -d "$(BIN)" ] || mkdir "$(BIN)"; \ 4 | if [ ! -f "$(BIN)/$(1)" ]; then \ 5 | echo "Downloading $(BIN)/$(1)"; \ 6 | curl --progress-bar -L $(2) | tar zxf - --wildcards --strip 1 -C $(BIN) '*/$(1)'; \ 7 | chmod +x "$(BIN)/$(1)"; \ 8 | fi; \ 9 | fi 10 | endef 11 | 12 | define dl_bin 13 | @if ! $(1) 2>/dev/null 1>&2; then \ 14 | [ -d "$(BIN)" ] || mkdir "$(BIN)"; \ 15 | if [ ! -f "$(BIN)/$(1)" ]; then \ 16 | echo "Downloading $(BIN)/$(1)"; \ 17 | curl --progress-bar -L $(2) --output "$(BIN)/$(1)"; \ 18 | chmod +x "$(BIN)/$(1)"; \ 19 | fi; \ 20 | fi 21 | endef 22 | 23 | MAKEFILE_PATH := $(abspath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))) 24 | DOCKER_COMPOSE_VERSION := 2.34.0 25 | ARCH := $(shell uname -m) 26 | OS := $(shell uname -s | tr A-Z a-z) 27 | 28 | ifndef BIN 29 | export BIN := $(MAKEFILE_PATH)/bin 30 | endif 31 | 32 | export PATH := $(BIN):$(PATH) 33 | 34 | 35 | DOCKER_COMPOSE_DOWNLOAD_URL := "https://github.com/docker/compose/releases/download/v$(DOCKER_COMPOSE_VERSION)/docker-compose-$(OS)-$(ARCH)" 36 | 37 | COMPOSE := docker-compose -f $(MAKEFILE_PATH)/docker/docker-compose.yml 38 | 39 | .PHONY: check 40 | check: check-autopep8 check-ruff 41 | 42 | .PHONY: fix 43 | fix: fix-autopep8 fix-ruff 44 | 45 | .PHONY: .prepare 46 | .prepare: 47 | @echo "======== Check and install missing dependencies" 48 | @pip install -r ./requirement-test.txt 49 | 50 | .PHONY: check-autopep8 51 | check-autopep8: .prepare 52 | @echo "======== Running autopep8 check" 53 | @autopep8 -r --diff ./ 54 | 55 | .PHONY: check-ruff 56 | check-ruff: .prepare 57 | @echo "======== Running ruff check" 58 | @ruff check 59 | 60 | .PHONY: fix-autopep8 61 | fix-autopep8: .prepare 62 | @echo "======== Running autopep8 fix" 63 | @autopep8 -r -j 4 -i ./ 64 | 65 | .PHONY: fix-ruff 66 | fix-ruff: .prepare 67 | @echo "======== Running ruff fix" 68 | @ruff check --fix --preview 69 | 70 | .PHONY: test 71 | test: unit-test integration-test 72 | 73 | .PHONY: unit-test 74 | unit-test: 75 | @echo "======== Running unit tests" 76 | @pytest ./test_unit* 77 | 78 | .PHONY: integration-test 79 | integration-test: scylla-up 80 | @echo "======== Running integration tests" 81 | @pytest ./test_integration* 82 | 83 | .PHONY: .prepare-cert 84 | .prepare-cert: 85 | @[ -f "docker/scylla/db.key" ] || (echo "Prepare certificate" && cd docker/scylla/ && openssl req -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -x509 -newkey rsa:4096 -keyout db.key -out db.crt -days 3650 -nodes) 86 | 87 | .PHONY: scylla-up 88 | scylla-up: .prepare-cert $(BIN)/docker-compose 89 | @sudo sysctl -w fs.aio-max-nr=10485760 90 | $(COMPOSE) up -d 91 | 92 | .PHONY: scylla-down 93 | scylla-down: $(BIN)/docker-compose 94 | $(COMPOSE) down 95 | 96 | .PHONY: scylla-kill 97 | scylla-kill: $(BIN)/docker-compose 98 | $(COMPOSE) kill 99 | 100 | .PHONY: scylla-clean 101 | scylla-clean: $(BIN)/docker-compose scylla-kill 102 | $(COMPOSE) rm 103 | 104 | $(BIN)/docker-compose: Makefile 105 | $(call dl_bin,docker-compose,$(DOCKER_COMPOSE_DOWNLOAD_URL)) 106 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Alternator - Client-side load balancing - Python 2 | 3 | ## Introduction 4 | As explained in the [toplevel README](../README.md), DynamoDB applications 5 | are usually aware of a _single endpoint_, a single URL to which they 6 | connect - e.g., `http://dynamodb.us-east-1.amazonaws.com`. But Alternator 7 | is distributed over a cluster of nodes and we would like the application to 8 | send requests to all these nodes - not just to one. This is important for two 9 | reasons: **high availability** (the failure of a single Alternator node should 10 | not prevent the client from proceeding) and **load balancing** over all 11 | Alternator nodes. 12 | 13 | One of the ways to do this is to provide a modified library, which will 14 | allow a mostly-unmodified application which is only aware of one 15 | "endpoint URL" to send its requests to many different Alternator nodes. 16 | 17 | Our intention is _not_ to fork the existing AWS client library for Python - 18 | **boto3**. Rather, our intention is to provide a small library which tacks 19 | on to any version of boto3 that the application is already using, and makes 20 | boto3 do the right thing for Alternator. 21 | 22 | ## The `alternator_lb` library 23 | Use `import alternator_lb` to make any boto3 client use Alternator cluster balancing load between nodes. 24 | This library periodically syncs list of active nodes with the cluster. 25 | 26 | ## Using the library 27 | 28 | ### Create new dynamodb botocore client 29 | 30 | ```python 31 | from alternator_lb import AlternatorLB, Config 32 | 33 | lb = AlternatorLB(Config(nodes=['x.x.x.x'], port=9999)) 34 | dynamodb = lb.new_botocore_dynamodb_client() 35 | 36 | dynamodb.delete_table(TableName="SomeTable") 37 | ``` 38 | 39 | ### Create new dynamodb boto3 client 40 | 41 | ```python 42 | from alternator_lb import AlternatorLB, Config 43 | 44 | lb = AlternatorLB(Config(nodes=['x.x.x.x'], port=9999)) 45 | dynamodb = lb.new_boto3_dynamodb_client() 46 | 47 | dynamodb.delete_table(TableName="SomeTable") 48 | ``` 49 | 50 | ### Rack and Datacenter awareness 51 | 52 | You can make it target nodes of particular datacenter or rack, as such: 53 | ```python 54 | lb = alternator_lb.AlternatorLB(['x.x.x.x'], port=9999, datacenter='dc1', rack='rack1') 55 | ``` 56 | 57 | You can also check if cluster knows datacenter and/or rack you are targeting: 58 | ```python 59 | try: 60 | lb.check_if_rack_and_datacenter_set_correctly() 61 | except ValueError: 62 | raise RuntimeError("Not supported") 63 | ``` 64 | 65 | This feature requires server support, you can check if server supports this feature: 66 | ```python 67 | try: 68 | supported = lb.check_if_rack_datacenter_feature_is_supported() 69 | except RuntimeError: 70 | raise RuntimeError("failed to check") 71 | ``` 72 | 73 | ## Examples 74 | 75 | Find more examples in `alternator_lb_tests.py` -------------------------------------------------------------------------------- /python/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | networks: 2 | public: 3 | name: alb_python_network 4 | driver: bridge 5 | ipam: 6 | driver: default 7 | config: 8 | - subnet: 172.43.0.0/16 9 | 10 | services: 11 | scylla1: 12 | image: scylladb/scylla:2025.1 13 | networks: 14 | public: 15 | ipv4_address: 172.43.0.2 16 | command: | 17 | --rpc-address 172.43.0.2 18 | --listen-address 172.43.0.2 19 | --seeds 172.43.0.2 20 | --skip-wait-for-gossip-to-settle 0 21 | --ring-delay-ms 0 22 | --smp 2 23 | --memory 1G 24 | healthcheck: 25 | test: [ "CMD", "cqlsh", "scylla1", "-e", "select * from system.local WHERE key='local'" ] 26 | interval: 5s 27 | timeout: 5s 28 | retries: 10 29 | ports: 30 | - "19042" 31 | - "9999" 32 | - "9998" 33 | volumes: 34 | - ./scylla:/etc/scylla 35 | scylla2: 36 | image: scylladb/scylla:2025.1 37 | networks: 38 | public: 39 | ipv4_address: 172.43.0.3 40 | command: | 41 | --rpc-address 172.43.0.3 42 | --listen-address 172.43.0.3 43 | --seeds 172.43.0.2 44 | --skip-wait-for-gossip-to-settle 0 45 | --ring-delay-ms 0 46 | --smp 2 47 | --memory 1G 48 | healthcheck: 49 | test: [ "CMD", "cqlsh", "scylla2", "-e", "select * from system.local WHERE key='local'" ] 50 | interval: 5s 51 | timeout: 5s 52 | retries: 10 53 | ports: 54 | - "19042" 55 | - "9999" 56 | - "9998" 57 | depends_on: 58 | scylla1: 59 | condition: service_healthy 60 | volumes: 61 | - ./scylla:/etc/scylla 62 | scylla3: 63 | image: scylladb/scylla:2025.1 64 | networks: 65 | public: 66 | ipv4_address: 172.43.0.4 67 | command: | 68 | --rpc-address 172.43.0.4 69 | --listen-address 172.43.0.4 70 | --seeds 172.43.0.2,172.43.0.3 71 | --skip-wait-for-gossip-to-settle 0 72 | --ring-delay-ms 0 73 | --smp 2 74 | --memory 1G 75 | healthcheck: 76 | test: [ "CMD", "cqlsh", "scylla3", "-e", "select * from system.local WHERE key='local'" ] 77 | interval: 5s 78 | timeout: 5s 79 | retries: 10 80 | ports: 81 | - "19042" 82 | - "9999" 83 | - "9998" 84 | depends_on: 85 | scylla2: 86 | condition: service_healthy 87 | volumes: 88 | - ./scylla:/etc/scylla -------------------------------------------------------------------------------- /python/requirement-test.txt: -------------------------------------------------------------------------------- 1 | pylint>=3.3.6 2 | boto3>=1.37.33 3 | botocore>=1.37.33 4 | autopep8>=2.3.2 5 | pytest>=8.3.5 6 | ruff>=0.11.5 7 | requests>=2.32.3 -------------------------------------------------------------------------------- /python/test_integration_boto3.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import urllib3.connection 4 | 5 | 6 | from alternator_lb import AlternatorLB, Config 7 | 8 | 9 | class TestAlternatorBotocore: 10 | initial_nodes = ['172.43.0.2'] 11 | http_port = 9998 12 | https_port = 9999 13 | 14 | def test_check_if_rack_datacenter_feature_is_supported(self): 15 | lb = AlternatorLB(Config(nodes=self.initial_nodes, 16 | port=self.http_port, datacenter="fake_dc")) 17 | lb.check_if_rack_datacenter_feature_is_supported() 18 | 19 | def test_check_if_rack_and_datacenter_set_correctly_wrong_dc(self): 20 | lb = AlternatorLB(Config(nodes=self.initial_nodes, 21 | port=self.http_port, datacenter="fake_dc")) 22 | try: 23 | lb.check_if_rack_and_datacenter_set_correctly() 24 | assert False, "Expected ValueError" 25 | except ValueError: 26 | pass 27 | 28 | def test_http_connection_persistent(self): 29 | self._test_connection_persistent("http", 1) 30 | self._test_connection_persistent("http", 2) 31 | 32 | def test_https_connection_persistent(self): 33 | self._test_connection_persistent("https", 1) 34 | self._test_connection_persistent("https", 2) 35 | 36 | def _test_connection_persistent(self, schema: str, max_pool_connections: int): 37 | cnt = 0 38 | if schema == "http": 39 | original_init = urllib3.connection.HTTPConnection.__init__ 40 | else: 41 | original_init = urllib3.connection.HTTPSConnection.__init__ 42 | 43 | def wrapper(self, *args, **kwargs): 44 | nonlocal cnt 45 | nonlocal original_init 46 | cnt += 1 47 | return original_init(self, *args, **kwargs) 48 | 49 | if schema == "http": 50 | patched = patch.object( 51 | urllib3.connection.HTTPConnection, '__init__', new=wrapper) 52 | else: 53 | patched = patch.object( 54 | urllib3.connection.HTTPSConnection, '__init__', new=wrapper) 55 | 56 | with patched: 57 | lb = AlternatorLB(Config( 58 | schema=schema, 59 | nodes=self.initial_nodes, 60 | port=self.http_port if schema == "http" else self.https_port, 61 | datacenter="fake_dc", 62 | update_interval=0, 63 | max_pool_connections=max_pool_connections, 64 | )) 65 | 66 | dynamodb = lb.new_boto3_dynamodb_client() 67 | try: 68 | dynamodb.delete_table(TableName="FakeTable") 69 | except Exception as e: 70 | if e.__class__.__name__ != "ResourceNotFoundException": 71 | raise 72 | assert cnt == 1 73 | try: 74 | dynamodb.delete_table(TableName="FakeTable") 75 | except Exception as e: 76 | if e.__class__.__name__ != "ResourceNotFoundException": 77 | raise 78 | assert cnt == 1 # Connection should be carried over to another request 79 | 80 | lb._update_live_nodes() 81 | assert cnt == 2 # AlternatorLB uses different connection pool, so one more connection will be created 82 | lb._update_live_nodes() 83 | assert cnt == 2 # And it should be carried over to another attempt of pulling nodes 84 | 85 | def test_check_if_rack_and_datacenter_set_correctly_correct_dc(self): 86 | lb = AlternatorLB(Config(nodes=self.initial_nodes, 87 | port=self.http_port, datacenter="datacenter1")) 88 | lb.check_if_rack_and_datacenter_set_correctly() 89 | 90 | @staticmethod 91 | def _run_create_add_delete_test(dynamodb): 92 | TABLE_NAME = "TestTable" 93 | ITEM_KEY = {'UserID': {'S': '123'}} 94 | 95 | try: 96 | dynamodb.delete_table(TableName=TABLE_NAME) 97 | except Exception as e: 98 | if e.__class__.__name__ != "ResourceNotFoundException": 99 | raise 100 | 101 | print("Creating table...") 102 | dynamodb.create_table( 103 | TableName=TABLE_NAME, 104 | KeySchema=[{'AttributeName': 'UserID', 105 | 'KeyType': 'HASH'}], # Primary Key 106 | AttributeDefinitions=[ 107 | {'AttributeName': 'UserID', 'AttributeType': 'S'}], # String Key 108 | ProvisionedThroughput={ 109 | 'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5} 110 | ) 111 | print(f"Table '{TABLE_NAME}' creation started.") 112 | 113 | # 2️⃣ Add an Item 114 | print("Adding item to the table...") 115 | dynamodb.put_item( 116 | TableName=TABLE_NAME, 117 | Item={ 118 | 'UserID': {'S': '123'}, 119 | 'Name': {'S': 'Alice'}, 120 | 'Age': {'N': '25'} 121 | } 122 | ) 123 | print("Item added.") 124 | 125 | # 3️⃣ Get the Item 126 | print("Retrieving item...") 127 | response = dynamodb.get_item(TableName=TABLE_NAME, Key=ITEM_KEY) 128 | if 'Item' in response: 129 | print("Retrieved Item:", response['Item']) 130 | else: 131 | print("Item not found.") 132 | 133 | # 4️⃣ Delete the Item 134 | print("Deleting item...") 135 | dynamodb.delete_item(TableName=TABLE_NAME, Key=ITEM_KEY) 136 | 137 | def test_botocore_create_add_delete(self): 138 | lb = AlternatorLB(Config( 139 | nodes=self.initial_nodes, 140 | port=self.http_port, 141 | datacenter="datacenter1", 142 | )) 143 | self._run_create_add_delete_test(lb.new_botocore_dynamodb_client()) 144 | 145 | def test_boto3_create_add_delete(self): 146 | lb = AlternatorLB(Config( 147 | nodes=self.initial_nodes, 148 | port=self.http_port, 149 | datacenter="datacenter1", 150 | )) 151 | self._run_create_add_delete_test(lb.new_boto3_dynamodb_client()) 152 | -------------------------------------------------------------------------------- /python/test_unit.py: -------------------------------------------------------------------------------- 1 | import gc 2 | 3 | 4 | def test_executor_pool(): 5 | from alternator_lb import ExecutorPool 6 | 7 | class FakePool: 8 | def __init__(self, *args, **kwargs): 9 | self.submit_counts = 0 10 | self.shutdown_counts = 0 11 | 12 | def submit(self, *args, **kwargs): 13 | self.submit_counts += 1 14 | 15 | def shutdown(self, *args, **kwargs): 16 | pass 17 | 18 | class ExecutorPoolPatch(ExecutorPool): 19 | @classmethod 20 | def create_executor(cls): 21 | return FakePool() 22 | 23 | def get_executor(self): 24 | return self._executor 25 | 26 | class SomeClass: 27 | pool = ExecutorPoolPatch() 28 | 29 | def __init__(self, *args, **kwargs): 30 | self.pool.add_ref() 31 | 32 | def __del__(self): 33 | self.pool.remove_ref() 34 | 35 | def submit(self, *args, **kwargs): 36 | self.pool.submit(*args, **kwargs) 37 | 38 | for _ in range(2): 39 | e = SomeClass() 40 | e1 = SomeClass() 41 | 42 | e.pool.submit(None) 43 | e1.pool.submit(None) 44 | 45 | executor = e.pool.get_executor() 46 | executor1 = e.pool.get_executor() 47 | assert executor1 == executor 48 | assert executor.submit_counts == 2 49 | 50 | del e 51 | del e1 52 | del executor1 53 | del executor 54 | gc.collect() 55 | 56 | assert SomeClass.pool.get_executor() is None 57 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------